From 0f7162baefb84952da808e83d7b8adf614a43570 Mon Sep 17 00:00:00 2001 From: Aaron Dewes Date: Thu, 17 Nov 2022 09:37:41 +0100 Subject: [PATCH 001/141] Allow _ in Lightning addresses This allows things like example_subdomain.example.com to be used as LN address --- src/common/lib/lnurl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/lib/lnurl.ts b/src/common/lib/lnurl.ts index 9c2aa07909..7caac2430e 100644 --- a/src/common/lib/lnurl.ts +++ b/src/common/lib/lnurl.ts @@ -16,7 +16,7 @@ const fromInternetIdentifier = (address: string) => { // email regex: https://emailregex.com/ if ( address.match( - /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-_0-9]+\.)+[a-zA-Z]{2,}))$/ ) ) { let [name, host] = address.split("@"); From 219b64007e7cac7668a35a1c8e83d1d868d9ae64 Mon Sep 17 00:00:00 2001 From: Aaron Dewes Date: Sun, 20 Nov 2022 10:45:56 +0100 Subject: [PATCH 002/141] Add comment --- src/common/lib/lnurl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/lib/lnurl.ts b/src/common/lib/lnurl.ts index 7caac2430e..0177dcb9f4 100644 --- a/src/common/lib/lnurl.ts +++ b/src/common/lib/lnurl.ts @@ -14,6 +14,7 @@ import { bech32Decode } from "../utils/helpers"; const fromInternetIdentifier = (address: string) => { // email regex: https://emailregex.com/ + // modified to allow _ in subdomains if ( address.match( /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-_0-9]+\.)+[a-zA-Z]{2,}))$/ From d7c1c2c1f08236433bf71a839b126be5ef72580c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=B6gel?= Date: Sat, 10 Dec 2022 21:53:46 +0100 Subject: [PATCH 003/141] refactor: Navbar: constrain max width to same width as the content. Reason: on desktop the navigation items in the top left and right corners were easy to miss. With this change, everything is closer together and easier to notice. --- src/app/components/Navbar/Navbar.tsx | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/app/components/Navbar/Navbar.tsx b/src/app/components/Navbar/Navbar.tsx index 2cc6a1dbee..7dc1e23935 100644 --- a/src/app/components/Navbar/Navbar.tsx +++ b/src/app/components/Navbar/Navbar.tsx @@ -7,17 +7,19 @@ type Props = { export default function Navbar({ children }: Props) { return ( -
-
- -
- {children && ( -
- +
+
+
+ +
+ {children && ( +
+ +
+ )} +
+
- )} -
-
); From 86c295a2dc1c73b950b500e8ad2f593b2eb0f712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=B6gel?= Date: Mon, 12 Dec 2022 11:40:36 +0100 Subject: [PATCH 004/141] + adjust horizontal padding. + remove superfluous
. --- src/app/components/Navbar/Navbar.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/app/components/Navbar/Navbar.tsx b/src/app/components/Navbar/Navbar.tsx index 7dc1e23935..731ec0401e 100644 --- a/src/app/components/Navbar/Navbar.tsx +++ b/src/app/components/Navbar/Navbar.tsx @@ -8,15 +8,11 @@ type Props = { export default function Navbar({ children }: Props) { return (
-
+
- {children && ( -
- -
- )} + {children && }
From 1a94125b97863543e98c07971c7b6911a679d260 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:56:06 +0000 Subject: [PATCH 005/141] Update dayjs to version 1.11.7 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b4e083e289..16cadf7063 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "bech32": "^2.0.0", "bolt11": "^1.4.0", "crypto-js": "^4.1.1", - "dayjs": "^1.11.6", + "dayjs": "^1.11.7", "dexie": "^3.2.2", "elliptic": "^6.5.4", "html5-qrcode": "^2.3.1", diff --git a/yarn.lock b/yarn.lock index 124877f981..81d2343351 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3399,10 +3399,10 @@ data-urls@^3.0.2: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" -dayjs@^1.11.6: - version "1.11.6" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.6.tgz#2e79a226314ec3ec904e3ee1dd5a4f5e5b1c7afb" - integrity sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ== +dayjs@^1.11.7: + version "1.11.7" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" + integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== debug@2.6.9, debug@^2.6.9: version "2.6.9" From 2af976cfc576a4075793add83671a8ed25cdf27d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?BSN=20=E2=88=9E/21M=20=E2=82=BF?= Date: Mon, 12 Dec 2022 12:30:59 +0000 Subject: [PATCH 006/141] Translated using Weblate (German) Currently translated at 22.3% (90 of 402 strings) Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/de/ --- src/i18n/locales/de/translation.json | 82 +++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 9261f0ae09..fd692a61ed 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -1,11 +1,11 @@ { "translation": { "welcome": { - "title": "Die Power von Lightning in Deinem Browser", + "title": "Die Power von Lightning in Ihrem Browser", "nav": { "welcome": "Willkommen", "password": "Dein Passwort", - "connect": "Mit Lightning verbinden", + "connect": "Ihr Lightning Konto", "done": "Fertig" }, "intro": { @@ -19,6 +19,84 @@ }, "privacy": { "title": "Datenschutz an erster Stelle" + }, + "foss": { + "title": "Kostenlos und Open Source", + "description": "Vollständig offener Code, der von jedermann überprüft werden kann. Keine Statistiken oder Tracker. Sie haben die Kontrolle." + } + }, + "set_password": { + "title": "Passwort zum Entsperren festlegen", + "description": "Die Daten Ihres Lightning-Kontos sind mit einem Freischalt-Passwort sicher verschlüsselt. Vergessen Sie dieses Passwort nicht! Sie benötigen es, um die Alby-Erweiterung zu entsperren (in diesem Browser)", + "choose_password": { + "label": "Wählen Sie ein Passwort zum Entsperren:" + } + }, + "test_connection": { + "try_tutorial": "Probieren Sie es jetzt aus", + "review_connection_details": "Bitte überprüfen Sie Ihre Verbindungsdaten.", + "actions": { + "delete_edit_account": "Ungültiges Konto löschen und erneut bearbeiten" + } + } + }, + "choose_connector": { + "alby": { + "pre_connect": { + "host_wallet": "Wir hosten die Lightning Wallet für Sie!", + "optional_lightning_address": { + "suffix": "@getalby.com" + } + } + }, + "lnd": { + "or": "OR", + "drag_and_drop": "Ziehe deine Makrone hierher oder legen Sie sie dort ab <0>durchsuchen", + "errors": { + "connection_failed": "Verbindung fehlgeschlagen. Sind deine LND-Anmeldeinformationen korrekt?" + }, + "url": { + "label": "REST-API-Host und -Port", + "placeholder": "https://Ihr-Knoten-URL:8080" + }, + "macaroon": { + "label": "Makrone (HEX-Format)" + }, + "page": { + "description": "Sie benötigen Ihre Knoten-URL und eine Macaroon mit Lese- und Sendeberechtigungen (z. B. admin.macaroon)" + } + }, + "lndhub_bluewallet": { + "uri": { + "label": "BlueWallet-Export-URI" + }, + "errors": { + "invalid_uri": "Ungültiger BlueWallet-URI" + } + }, + "lndhub_go": { + "title": "LNDHub", + "uri": { + "label": "LNDHub-Export-URI" + }, + "errors": { + "invalid_uri": "Ungültiger LNDHub-URI", + "connection_failed": "Verbindung fehlgeschlagen. Ist dein LNDHub-URI korrekt?" + }, + "description": "Verbinden dich mit deinem LNDHub-Konto", + "page": { + "title": "Verbinden dich mit LNDHub", + "description": "Gebe hier deine LNDHub-Zugangsdaten-URI ein oder scannen den QR-Code mit deiner Webcam." + } + }, + "lnbits": { + "admin_key": { + "label": "LNbits-Admin-Schlüssel" + }, + "title": "LNbits", + "description": "Verbinde dich mit deinem LNbits-Konto", + "page": { + "title": "Verbinde dich mit <0>LNbits" } } } From 6c55a6ce8686eec9ec9acec0f74db1cb2134127e Mon Sep 17 00:00:00 2001 From: Blocky Date: Mon, 12 Dec 2022 11:00:14 +0000 Subject: [PATCH 007/141] Translated using Weblate (German) Currently translated at 22.3% (90 of 402 strings) Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/de/ --- src/i18n/locales/de/translation.json | 123 +++++++++++++++++++++++++-- 1 file changed, 114 insertions(+), 9 deletions(-) diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index fd692a61ed..e47edb46fa 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -18,11 +18,15 @@ "description": "Definieren Sie individuelle Budgets für Websites, um nahtlose Zahlungsströme zu ermöglichen. Keine lästigen Paywalls mehr." }, "privacy": { - "title": "Datenschutz an erster Stelle" + "title": "Datenschutz an erster Stelle", + "description": "Verwende lightning, um die zu authentifizieren und dein Privatsphäre zu kontrollieren." }, "foss": { "title": "Kostenlos und Open Source", "description": "Vollständig offener Code, der von jedermann überprüft werden kann. Keine Statistiken oder Tracker. Sie haben die Kontrolle." + }, + "actions": { + "get_started": "Beginne" } }, "set_password": { @@ -30,6 +34,14 @@ "description": "Die Daten Ihres Lightning-Kontos sind mit einem Freischalt-Passwort sicher verschlüsselt. Vergessen Sie dieses Passwort nicht! Sie benötigen es, um die Alby-Erweiterung zu entsperren (in diesem Browser)", "choose_password": { "label": "Wählen Sie ein Passwort zum Entsperren:" + }, + "errors": { + "mismatched_password": "Die Passwörter stimmen nicht überein.", + "enter_password": "Bitte gib ein Passwort ein.", + "confirm_password": "Bitte bestätige dein Passwort." + }, + "confirm_password": { + "label": "Wiederhole deine Passworteingabe" } }, "test_connection": { @@ -37,7 +49,13 @@ "review_connection_details": "Bitte überprüfen Sie Ihre Verbindungsdaten.", "actions": { "delete_edit_account": "Ungültiges Konto löschen und erneut bearbeiten" - } + }, + "ready": "Super, du hast es geschafft, es kann losgehen!", + "tutorial": "Du hast jetzt dein Wallet verbunden, möchtest du ein Tutorial starten?", + "initializing": "Dein Account wird initialisiert. Bitte warte, das kann ein wenig dauern.", + "connection_error": "Verbindungsfehler", + "connection_taking_long": "Das Verbinden dauert länger als erwartet...sind deine Angaben richtig? Ist dein Node erreichbar?", + "contact_support": "Wenn du Hilfe brauchst, kontaktiere bitte support@getalby.com" } }, "choose_connector": { @@ -45,9 +63,26 @@ "pre_connect": { "host_wallet": "Wir hosten die Lightning Wallet für Sie!", "optional_lightning_address": { - "suffix": "@getalby.com" + "suffix": "@getalby.com", + "title": "Zahlen und Buchstaben, mindestens 3 Ziffern", + "label": "Wähle deine Lightning Adresse (optional)" + }, + "optional_lightning_note": { + "part2": "Lightning Adresse", + "part3": "Das ist für jeden ein einfacher Weg, um dir Bitcoin über das Lightning Netzwerk zu schicken.", + "part4": "Erfahre mehr" + }, + "title": "Dein Alby Lightning Wallet", + "login_account": "Erstelle einen Alby Account oder logge dich ein.", + "errors": { + "create_wallet_error": "Der Login oder das Erstellen eines Accounts war nicht möglich. Wenn du Hilfe brauchst, kontaktiere bitte support@getalby.com" + }, + "email": { + "label": "E-Mail Adresse" } - } + }, + "title": "Alby Wallet", + "description": "Erstelle einen Alby Account oder logge dich ein" }, "lnd": { "or": "OR", @@ -63,15 +98,25 @@ "label": "Makrone (HEX-Format)" }, "page": { - "description": "Sie benötigen Ihre Knoten-URL und eine Macaroon mit Lese- und Sendeberechtigungen (z. B. admin.macaroon)" - } + "description": "Sie benötigen Ihre Knoten-URL und eine Macaroon mit Lese- und Sendeberechtigungen (z. B. admin.macaroon)", + "title": "Verbinde dich zu deinem LND Node" + }, + "description": "Verbinde dich mit deinem LND Node", + "title": "LND" }, "lndhub_bluewallet": { "uri": { "label": "BlueWallet-Export-URI" }, "errors": { - "invalid_uri": "Ungültiger BlueWallet-URI" + "invalid_uri": "Ungültiger BlueWallet-URI", + "connection_failed": "Verbindung nicht erfolgreich. Ist deine BlueWallet URI richtig?" + }, + "title": "Bluewallet", + "description": "Verbinde dich mit deiner mobilen Bluewallet App", + "page": { + "title": "Verbinde dich mit Bluewallet", + "description": "Wähle in der BlueWallet die Wallet, die du verbinden willst, öffne sie, klicke auf \"...\", klicke auf Export/Backup, um den QR Code anzuzeigen. Scanne ihn dann mit deiner Webcam." } }, "lndhub_go": { @@ -91,14 +136,74 @@ }, "lnbits": { "admin_key": { - "label": "LNbits-Admin-Schlüssel" + "label": "LNbits-Admin-Schlüssel", + "placeholder": "Dein 32-stelliger admin key" }, "title": "LNbits", "description": "Verbinde dich mit deinem LNbits-Konto", "page": { - "title": "Verbinde dich mit <0>LNbits" + "title": "Verbinde dich mit <0>LNbits", + "instructions": "Wähle in LNbits die Wallet, die du verbinden willst, öffne sie, klicke auf API Info und kopiere den Admin Key. Füge ihn unten ein:" + } + }, + "title": { + "welcome": "Hast du eine Lightning Wallet?", + "options": "Füge einen neuen Lightning Account hinzu" + }, + "mynode": { + "page": { + "instructions": "Klicke auf deiner myNode Übersicht auf den Button <0>Wallet für deinen <0>Lightning service. Klicke jetzt auf den Button <0>Pair Wallet unterhalb dem Tab <0>Status . Gib dein Passwort ein, wenn es verlang wird. <1/> Wähle das Dropdown Menü und entscheide dich für eine pairing option. Abhängig von deinem Setup kannst du entweder für lokale Verbindung wählen <0>Lightning (REST + Local IP) oder für TOR die Auswahl <0>Lightning (REST + Tor)." + } + }, + "umbrel": { + "page": { + "instructions": "Gehe im Umbrel Dashboard zu <0>Connect Wallet. Wähle <0>lndconnect REST und kopiere <0>lndconnect URL. (Abhängig von deinem Setup kannst du entweder eine lokale Verbindung verwenden oder die Verbindung über TOR.)" + } + }, + "description": "Du musst dich zuerst mit einer Lightning Wallet verbinden, damit du dich mit deinen Lieblingswebseiten verbinden kannst, die Bitcoin Lightning Zahlungen akzeptieren!", + "citadel": { + "page": { + "instructions": "Das funktioniert aktuell nicht, wenn 2FA ausgewählt ist." + } + } + }, + "unlock": { + "unlock_error": { + "link": "Zurücksetzen und neuen Account erstellen" + }, + "errors": { + "invalid_password": "ungültiges Passwort" + } + }, + "settings": { + "nostr": { + "private_key": { + "warning": "Das wird deinen alten Private Key löschen. Bist du sicher?" } } } + }, + "components": { + "account_menu": { + "options": { + "account": { + "add": "Füge einen neuen Account hinzu" + } + } + }, + "publishers_table": { + "payments": "Zahlungen" + }, + "toasts": { + "connection_error": { + "double_check": "Überprüfe nochmals deine Verbindungseinstellungen" + }, + "errors": { + "invalid_credentials": "Falsches Passwort. Überprüfe das Passwort und E-Mail Adresse und versuche es noch einmal." + } + }, + "companion_download_info": { + "using_tor": "Klicke hier zum Weitermachen, wenn du den TOR Browser verwendest." + } } } From bf0f182f4aeb2271fe181d9730dc0448db18f9fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?BSN=20=E2=88=9E/21M=20=E2=82=BF?= Date: Mon, 12 Dec 2022 13:45:37 +0000 Subject: [PATCH 008/141] Translated using Weblate (German) Currently translated at 34.0% (137 of 402 strings) Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/de/ --- src/i18n/locales/de/translation.json | 107 +++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 6 deletions(-) diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index e47edb46fa..329f90b333 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -137,13 +137,19 @@ "lnbits": { "admin_key": { "label": "LNbits-Admin-Schlüssel", - "placeholder": "Dein 32-stelliger admin key" + "placeholder": "Ihr 32-stelliger Admin-Schlüssel" }, "title": "LNbits", "description": "Verbinde dich mit deinem LNbits-Konto", "page": { "title": "Verbinde dich mit <0>LNbits", "instructions": "Wähle in LNbits die Wallet, die du verbinden willst, öffne sie, klicke auf API Info und kopiere den Admin Key. Füge ihn unten ein:" + }, + "url": { + "label": "LNbits-URL" + }, + "errors": { + "connection_failed": "Verbindung fehlgeschlagen. Hast du die richtige URL und den richtigen Admin-Schlüssel?" } }, "title": { @@ -152,18 +158,107 @@ }, "mynode": { "page": { - "instructions": "Klicke auf deiner myNode Übersicht auf den Button <0>Wallet für deinen <0>Lightning service. Klicke jetzt auf den Button <0>Pair Wallet unterhalb dem Tab <0>Status . Gib dein Passwort ein, wenn es verlang wird. <1/> Wähle das Dropdown Menü und entscheide dich für eine pairing option. Abhängig von deinem Setup kannst du entweder für lokale Verbindung wählen <0>Lightning (REST + Local IP) oder für TOR die Auswahl <0>Lightning (REST + Tor)." - } + "instructions": "Klicke auf deiner myNode Übersicht auf den Button <0>Wallet für deinen <0>Lightning service. Klicke jetzt auf den Button <0>Pair Wallet unterhalb dem Tab <0>Status . Gib dein Passwort ein, wenn es verlang wird. <1/> Wähle das Dropdown Menü und entscheide dich für eine pairing option. Abhängig von deinem Setup kannst du entweder für lokale Verbindung wählen <0>Lightning (REST + Local IP) oder für TOR die Auswahl <0>Lightning (REST + Tor).", + "title": "Verbinde dich mit <0>myNode" + }, + "rest_url": { + "label": "lndconnect REST URL", + "placeholder": "lndconnect://yournode:8080?..." + }, + "description": "Verbinde dich mit deiner myNode", + "title": "myNode" }, "umbrel": { "page": { - "instructions": "Gehe im Umbrel Dashboard zu <0>Connect Wallet. Wähle <0>lndconnect REST und kopiere <0>lndconnect URL. (Abhängig von deinem Setup kannst du entweder eine lokale Verbindung verwenden oder die Verbindung über TOR.)" - } + "instructions": "Gehe in deine Regenschirm Seite zu <0>verbinde Wallet. Wähle <0>lndconnect REST und kopiere <0>lndconnect URL. (Abhängig von deinem Setup kannst du entweder eine lokale Verbindung verwenden oder die Verbindung über TOR.)", + "title": "Verbinde dich mit deinem <0>Regenschirm-Knoten" + }, + "rest_url": { + "label": "lndconnect REST URL", + "placeholder": "lndconnect://deinknoten:8080?..." + }, + "title": "Regenschirm", + "description": "Verbinde dich mit deinem Regenschirm" }, "description": "Du musst dich zuerst mit einer Lightning Wallet verbinden, damit du dich mit deinen Lieblingswebseiten verbinden kannst, die Bitcoin Lightning Zahlungen akzeptieren!", "citadel": { "page": { - "instructions": "Das funktioniert aktuell nicht, wenn 2FA ausgewählt ist." + "instructions": "Das funktioniert aktuell nicht, wenn 2FA ausgewählt ist.", + "title": "Verbinden dich mit dem Knoten <0>Zitadelle" + }, + "description": "Verbinde dich mit deiner lokalen Zitadelle", + "title": "Zitadelle", + "password": { + "label": "Zitadelle Passwort" + }, + "url": { + "label": "Zitadelle URL", + "placeholder": "http://zitadelle.local" + } + }, + "raspiblitz": { + "page": { + "instructions1": "Du benötigst deine Node-Onion-Adresse, Port und eine Macaroon mit Lese- und Sendeberechtigungen (z. B. admin.macaroon).<1/><1/><0>SSH in deinem <0>RaspiBlitz.<1/>Führe den Befehl <0>sudo cat /mnt/hdd/tor/lndrest/hostname aus.<1/>Kopiere die <0>.onion-Adresse und füge deine Eingabe unten ein .<1/>Füge deinen <0>Port nach der Onion-Adresse hinzu, der Standardport ist <0>:8080.", + "title": "Verbinde dich mit deinem <0>RaspiBlitz-Knoten", + "instructions2": "Wähle <0>VERBINDEN.<1/>Wähle <0>EXPORTIEREN.<1/>Wähle <0>HEX.<1/>Kopiere das <0>adminMacaroon.<1/>Füge die Makrone in die Eingabe unten ein." + }, + "title": "RaspiBlitz", + "description": "Verbinde dich mit deinem RaspiBlitz", + "rest_api_host": { + "label": "REST-API-Host", + "placeholder": "Ihr-Knoten-Onion-Adresse: Port" + } + }, + "eclair": { + "title": "Eclair", + "description": "Verbinde dich mit deinem Eclair-Knoten", + "page": { + "title": "Verbinden dich mit <0>Eclair", + "instructions": "Du benötigst deine Eclair-URL und dein Passwort." + }, + "password": { + "label": "Eclair Passwort" + }, + "url": { + "label": "Eclair URL", + "placeholder": "http://localhost:8080" + } + }, + "start9": { + "page": { + "instructions": "<0>Hinweis: Derzeit unterstützen wir nur LND, aber wir werden in Zukunft c-lightning-Unterstützung hinzufügen!<1/>Klicke auf deinem Embassy-Dashboard auf den Dienst <0>Lightning Network Daemon. <1/>Wählen die Registerkarte <0>Eigenschaften.<1/>Kopiere nun die <0>LND Connect REST URL.", + "title": "Verbinde dich mit deinem <0>Embassy-Knoten" + }, + "rest_url": { + "placeholder": "lndconnect://deinknoten:8080?...", + "label": "lndconnect REST URL" + }, + "title": "Start9", + "description": "Verbinde dich mit Embassy" + }, + "bitcoin_beach": { + "title": "Bitcoin Beach Wallet", + "description": "Erstelle oder verbinde dich mit einem Bitcoin Beach (Galoy)-Konto", + "page": { + "title": "Verbinde dich mit <0>Bitcoin Beach Wallet" + } + }, + "bitcoin_jungle": { + "title": "Bitcoin Jungle Wallet", + "description": "Erstelle oder verbinde dich mit einem Bitcoin Jungle (Galoy)-Konto", + "page": { + "title": "Verbinde dich mit <0>Bitcoin Jungle Wallet" + } + }, + "galoy": { + "phone_number": { + "label": "Trage deine Telefonnummer ein" + }, + "sms_code": { + "label": "Gib deinen SMS-Bestätigungscode ein" + }, + "jwt": { + "label": "Gebe deinen JWT-Token ein" } } }, From edaa2082c6119d8dbf128ec8cb6e2ced6c17681d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?BSN=20=E2=88=9E/21M=20=E2=82=BF?= Date: Mon, 12 Dec 2022 17:55:03 +0000 Subject: [PATCH 009/141] Translated using Weblate (German) Currently translated at 48.7% (196 of 402 strings) Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/de/ --- src/i18n/locales/de/translation.json | 195 ++++++++++++++++++++++----- 1 file changed, 159 insertions(+), 36 deletions(-) diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 329f90b333..6dda99bd70 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -1,11 +1,11 @@ { "translation": { "welcome": { - "title": "Die Power von Lightning in Ihrem Browser", + "title": "Die Macht von ⚡ Bitcoin ⚡ in Ihrem Browser", "nav": { "welcome": "Willkommen", "password": "Dein Passwort", - "connect": "Ihr Lightning Konto", + "connect": "Dein Lightning-Konto", "done": "Fertig" }, "intro": { @@ -19,11 +19,11 @@ }, "privacy": { "title": "Datenschutz an erster Stelle", - "description": "Verwende lightning, um die zu authentifizieren und dein Privatsphäre zu kontrollieren." + "description": "Verwende Lightning, um dich zu authentifizieren und deine Privatsphäre zu kontrollieren." }, "foss": { "title": "Kostenlos und Open Source", - "description": "Vollständig offener Code, der von jedermann überprüft werden kann. Keine Statistiken oder Tracker. Sie haben die Kontrolle." + "description": "Vollständig offener Code, der von jedermann überprüft werden kann. Keine Statistiken oder Tracker. Du hast die Kontrolle." }, "actions": { "get_started": "Beginne" @@ -31,9 +31,9 @@ }, "set_password": { "title": "Passwort zum Entsperren festlegen", - "description": "Die Daten Ihres Lightning-Kontos sind mit einem Freischalt-Passwort sicher verschlüsselt. Vergessen Sie dieses Passwort nicht! Sie benötigen es, um die Alby-Erweiterung zu entsperren (in diesem Browser)", + "description": "Die Daten deines Lightning-Kontos sind mit einem Freischalt-Passwort sicher verschlüsselt. Vergesse das Passwort nicht! Du benötigst es, um die Alby-Erweiterung zu entsperren (in diesem Browser)", "choose_password": { - "label": "Wählen Sie ein Passwort zum Entsperren:" + "label": "Wähle ein Passwort zum Entsperren:" }, "errors": { "mismatched_password": "Die Passwörter stimmen nicht überein.", @@ -41,17 +41,17 @@ "confirm_password": "Bitte bestätige dein Passwort." }, "confirm_password": { - "label": "Wiederhole deine Passworteingabe" + "label": "Wiederhole deine Passworteingabe:" } }, "test_connection": { - "try_tutorial": "Probieren Sie es jetzt aus", - "review_connection_details": "Bitte überprüfen Sie Ihre Verbindungsdaten.", + "try_tutorial": "Probiere es jetzt aus", + "review_connection_details": "Bitte überprüfe deine Verbindungsdaten.", "actions": { "delete_edit_account": "Ungültiges Konto löschen und erneut bearbeiten" }, "ready": "Super, du hast es geschafft, es kann losgehen!", - "tutorial": "Du hast jetzt dein Wallet verbunden, möchtest du ein Tutorial starten?", + "tutorial": "Du hast jetzt deine Wallet verbunden, möchtest du ein Tutorial starten?", "initializing": "Dein Account wird initialisiert. Bitte warte, das kann ein wenig dauern.", "connection_error": "Verbindungsfehler", "connection_taking_long": "Das Verbinden dauert länger als erwartet...sind deine Angaben richtig? Ist dein Node erreichbar?", @@ -61,7 +61,7 @@ "choose_connector": { "alby": { "pre_connect": { - "host_wallet": "Wir hosten die Lightning Wallet für Sie!", + "host_wallet": "Wir hosten die Lightning Wallet für dich!", "optional_lightning_address": { "suffix": "@getalby.com", "title": "Zahlen und Buchstaben, mindestens 3 Ziffern", @@ -70,23 +70,24 @@ "optional_lightning_note": { "part2": "Lightning Adresse", "part3": "Das ist für jeden ein einfacher Weg, um dir Bitcoin über das Lightning Netzwerk zu schicken.", - "part4": "Erfahre mehr" + "part4": "Erfahre mehr", + "part1": "Dein Alby-Konto wird auch mit einem optionalen" }, - "title": "Dein Alby Lightning Wallet", + "title": "Deine Alby Lightning Wallet", "login_account": "Erstelle einen Alby Account oder logge dich ein.", "errors": { - "create_wallet_error": "Der Login oder das Erstellen eines Accounts war nicht möglich. Wenn du Hilfe brauchst, kontaktiere bitte support@getalby.com" + "create_wallet_error": "Der Login oder das Erstellen eines Accounts waren nicht möglich. Wenn du Hilfe brauchst, kontaktiere bitte support@getalby.com" }, "email": { "label": "E-Mail Adresse" } }, - "title": "Alby Wallet", + "title": "Alby Brieftasche", "description": "Erstelle einen Alby Account oder logge dich ein" }, "lnd": { "or": "OR", - "drag_and_drop": "Ziehe deine Makrone hierher oder legen Sie sie dort ab <0>durchsuchen", + "drag_and_drop": "Ziehe deine Makrone hierher oder lege sie dort ab <0>durchsuchen", "errors": { "connection_failed": "Verbindung fehlgeschlagen. Sind deine LND-Anmeldeinformationen korrekt?" }, @@ -98,10 +99,10 @@ "label": "Makrone (HEX-Format)" }, "page": { - "description": "Sie benötigen Ihre Knoten-URL und eine Macaroon mit Lese- und Sendeberechtigungen (z. B. admin.macaroon)", + "description": "Du benötigst deinen URL-Knoten und eine Macaroon mit Lese- und Sendeberechtigungen (z.B. admin.macaroon)", "title": "Verbinde dich zu deinem LND Node" }, - "description": "Verbinde dich mit deinem LND Node", + "description": "Verbinde dich mit deiner LND Node", "title": "LND" }, "lndhub_bluewallet": { @@ -109,10 +110,10 @@ "label": "BlueWallet-Export-URI" }, "errors": { - "invalid_uri": "Ungültiger BlueWallet-URI", + "invalid_uri": "Ungültige BlueWallet-URI", "connection_failed": "Verbindung nicht erfolgreich. Ist deine BlueWallet URI richtig?" }, - "title": "Bluewallet", + "title": "BlueWallet", "description": "Verbinde dich mit deiner mobilen Bluewallet App", "page": { "title": "Verbinde dich mit Bluewallet", @@ -131,7 +132,7 @@ "description": "Verbinden dich mit deinem LNDHub-Konto", "page": { "title": "Verbinden dich mit LNDHub", - "description": "Gebe hier deine LNDHub-Zugangsdaten-URI ein oder scannen den QR-Code mit deiner Webcam." + "description": "Gebe hier deine LNDHub-Zugangsdaten-URI ein oder scanne den QR-Code mit deiner Webcam." } }, "lnbits": { @@ -158,15 +159,15 @@ }, "mynode": { "page": { - "instructions": "Klicke auf deiner myNode Übersicht auf den Button <0>Wallet für deinen <0>Lightning service. Klicke jetzt auf den Button <0>Pair Wallet unterhalb dem Tab <0>Status . Gib dein Passwort ein, wenn es verlang wird. <1/> Wähle das Dropdown Menü und entscheide dich für eine pairing option. Abhängig von deinem Setup kannst du entweder für lokale Verbindung wählen <0>Lightning (REST + Local IP) oder für TOR die Auswahl <0>Lightning (REST + Tor).", + "instructions": "Klicke auf deiner myNode Übersicht auf den Button <0>Wallet für deinen <0>Lightning Service. Klicke jetzt auf den Button <0>Pair Wallet unterhalb dem Tab <0>Status . Gib dein Passwort ein, wenn es verlang wird. <1/> Wähle das Dropdown Menü und entscheide dich für eine Pairingoption. Abhängig von deinem Setup kannst du entweder für lokale Verbindung wählen <0>Lightning (REST + Local IP) oder für TOR die Auswahl <0>Lightning (REST + Tor).", "title": "Verbinde dich mit <0>myNode" }, "rest_url": { - "label": "lndconnect REST URL", - "placeholder": "lndconnect://yournode:8080?..." + "label": "Indconnect REST URL", + "placeholder": "Indconnect://IhrKnoten:8080?..." }, "description": "Verbinde dich mit deiner myNode", - "title": "myNode" + "title": "meinKnoten" }, "umbrel": { "page": { @@ -174,7 +175,7 @@ "title": "Verbinde dich mit deinem <0>Regenschirm-Knoten" }, "rest_url": { - "label": "lndconnect REST URL", + "label": "Indconnect-REST-URL", "placeholder": "lndconnect://deinknoten:8080?..." }, "title": "Regenschirm", @@ -184,7 +185,7 @@ "citadel": { "page": { "instructions": "Das funktioniert aktuell nicht, wenn 2FA ausgewählt ist.", - "title": "Verbinden dich mit dem Knoten <0>Zitadelle" + "title": "Verbinde dich mit dem Knoten <0>Zitadelle" }, "description": "Verbinde dich mit deiner lokalen Zitadelle", "title": "Zitadelle", @@ -202,7 +203,7 @@ "title": "Verbinde dich mit deinem <0>RaspiBlitz-Knoten", "instructions2": "Wähle <0>VERBINDEN.<1/>Wähle <0>EXPORTIEREN.<1/>Wähle <0>HEX.<1/>Kopiere das <0>adminMacaroon.<1/>Füge die Makrone in die Eingabe unten ein." }, - "title": "RaspiBlitz", + "title": "Raspiblitz", "description": "Verbinde dich mit deinem RaspiBlitz", "rest_api_host": { "label": "REST-API-Host", @@ -210,17 +211,17 @@ } }, "eclair": { - "title": "Eclair", + "title": "Eclair Brieftasche", "description": "Verbinde dich mit deinem Eclair-Knoten", "page": { - "title": "Verbinden dich mit <0>Eclair", + "title": "Verbinde dich mit <0>Eclair", "instructions": "Du benötigst deine Eclair-URL und dein Passwort." }, "password": { "label": "Eclair Passwort" }, "url": { - "label": "Eclair URL", + "label": "Eclair Brieftasche URL", "placeholder": "http://localhost:8080" } }, @@ -231,20 +232,20 @@ }, "rest_url": { "placeholder": "lndconnect://deinknoten:8080?...", - "label": "lndconnect REST URL" + "label": "Indconnect-REST-URL" }, "title": "Start9", "description": "Verbinde dich mit Embassy" }, "bitcoin_beach": { - "title": "Bitcoin Beach Wallet", + "title": "Bitcoin Beach Brieftasche", "description": "Erstelle oder verbinde dich mit einem Bitcoin Beach (Galoy)-Konto", "page": { "title": "Verbinde dich mit <0>Bitcoin Beach Wallet" } }, "bitcoin_jungle": { - "title": "Bitcoin Jungle Wallet", + "title": "Bitcoin Jungle Geldbörse", "description": "Erstelle oder verbinde dich mit einem Bitcoin Jungle (Galoy)-Konto", "page": { "title": "Verbinde dich mit <0>Bitcoin Jungle Wallet" @@ -258,7 +259,64 @@ "label": "Gib deinen SMS-Bestätigungscode ein" }, "jwt": { - "label": "Gebe deinen JWT-Token ein" + "label": "Gebe deinen JWT-Token ein", + "info": "Das {{label}}-Login wird derzeit aktualisiert. Wenn du ein fortgeschrittener Benutzer bist, kannst du dir dein JWT-Token holen, indem du über <0>Web Wallet (wallet.mainnet.galoy.io)<1/><1/> anmeldest. Das JWT sieht so aus: < 2>eyJhbG...<1/><1/>" + }, + "actions": { + "login": "Anmeldung", + "request_sms_code": "SMS-Code anfordern" + }, + "errors": { + "failed_to_request_sms": "SMS-Code konnte nicht angefordert werden", + "failed_to_login_sms": "Anmeldung mit SMS-Code fehlgeschlagen", + "setup_failed": "Einrichtung fehlgeschlagen", + "missing_jwt": "JWT fehlt, Anmeldung nicht möglich." + } + }, + "btcpay": { + "title": "BTCPay-Server", + "page": { + "title": "Verbinde dich mit deinem BTCPay LND-Knoten", + "instructions": "Navigiere zu deinem BTCPayServer und melde dich als Administrator an. Gehen zu Servereinstellungen > Dienste > LND-Rest – Siehe Informationen. Klicke dann auf „QR-Code-Informationen anzeigen“ und kopiere die QR-Code-Daten. Füge es unten ein:" + }, + "description": "Verbinde dich mit deinem BTCPay LND-Knoten", + "config": { + "label": "Konfigurationsdaten" + }, + "errors": { + "connection_failed": "Verbindung fehlgeschlagen. Ist die BTCPay-Verbindungs-URL korrekt und zugänglich?" + } + }, + "commando": { + "title": "Core Lightning", + "description": "Stelle eine Verbindung zu deinem Core Lightning-Knoten her", + "page": { + "title": "Stelle eine Verbindung zu deinem Core Lightning-Knoten her", + "instructions": "Stelle sicher, dass du Core Lightning Version 0.12.0 oder neuer hast, das Commando-Plugin ausgeführt wird und dein Knoten über das Lightning-Netzwerk zugänglich ist. Erstelle eine Rune, indem du „lightning-cli commando-rune“ ausführst." + }, + "host": { + "label": "Host" + }, + "pubkey": { + "label": "Öffentlicher Schlüssel" + }, + "rune": { + "label": "Rune" + }, + "port": { + "label": "Port" + }, + "proxy": { + "label": "Websocket-Proxy" + }, + "privKey": { + "label": "Lokaler privater Schlüssel (automatisch generiert)" + }, + "config": { + "label": "Konfigurationsdaten" + }, + "errors": { + "connection_failed": "Verbindung fehlgeschlagen. Ist dein Core Lightning-Knoten online und verwendet es das Commando-Plugin?" } } }, @@ -276,6 +334,71 @@ "warning": "Das wird deinen alten Private Key löschen. Bist du sicher?" } } + }, + "home": { + "default_view": { + "is_blocked_hint": "Alby ist derzeit auf dem {{host}} deaktiviert", + "recent_transactions": "kürzliche Transaktionen", + "no_transactions": "Es wurden noch keine Transaktionen mit Alby getätigt.", + "block_removed": "{{Host}} aktiviert. Bitte laden Sie die Website neu." + }, + "allowance_view": { + "sats_used": "verwendete Sats", + "no_transactions": "Noch keine Transaktionen auf <0>{{name}}.", + "recent_transactions": "kürzliche Transaktionen", + "allowance": "Erlaubnis" + }, + "actions": { + "send_satoshis": "⚡️ Satoshis senden ⚡️", + "enable_now": "Jetzt aktivieren" + }, + "transaction_list": { + "tabs": { + "outgoing": "Ausgehend", + "incoming": "Eingehend" + } + }, + "recent_transactions": "kürzliche Transaktionen" + }, + "accounts": { + "title": "Konten", + "export": { + "title": "Konto exportieren", + "screen_reader": "Kontodaten exportieren", + "waiting": "Warten auf LndHub-Daten...", + "scan_qr": "Importiere diese Brieftasche in Zeus oder BlueWallet, indem du den QRCode scannst.", + "your_ln_address": "Deine Lightning Adresse:", + "tip_mobile": "Tipp: Verwende diese Geldbörse mit deinem Mobilgerät", + "export_uri": "LNDHub Anmeldeinformationen URI" + }, + "edit": { + "name": { + "label": "Name" + }, + "screen_reader": "Kontonamen bearbeiten", + "title": "Konto bearbeiten" + }, + "remove": { + "confirm": "Bist du dir sicher, dass du das Konto entfernen möchten: {{Name}}? \nDies kann nicht rückgängig gemacht werden. Wenn du dich mit diesem Konto bei Webseiten angemeldet hast, verlierst du möglicherweise den Zugriff auf diese." + }, + "actions": { + "add_account": "Konto hinzufügen" + } + }, + "enable": { + "allow": "Erlaube du {{host}} zu:", + "title": "Verbinden", + "request1": "Genehmigung für Transaktionen anfordern", + "request2": "Fordern Sie Rechnungen und Lightning Informationen an", + "block_and_ignore": "Blockieren und Ignorieren von {{host}}" + }, + "receive": { + "amount": { + "placeholder": "Betrag in Satoshi..." + }, + "payment": { + "waiting": "Warten auf Zahlung..." + } } }, "components": { @@ -298,7 +421,7 @@ } }, "companion_download_info": { - "using_tor": "Klicke hier zum Weitermachen, wenn du den TOR Browser verwendest." + "using_tor": "Klicke hier zum Weitermachen, wenn du den TOR Browser verwendest" } } } From 32abc5b84dce49cbc4ce41ebed8ad920d56c8e6d Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Tue, 13 Dec 2022 16:30:19 +0000 Subject: [PATCH 010/141] Update @types/chrome to version 0.0.204 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 16cadf7063..8893204d23 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", "@trivago/prettier-plugin-sort-imports": "^4.0.0", - "@types/chrome": "^0.0.203", + "@types/chrome": "^0.0.204", "@types/crypto-js": "^4.1.1", "@types/elliptic": "^6.4.14", "@types/lodash.merge": "^4.6.7", diff --git a/yarn.lock b/yarn.lock index 81d2343351..9017668061 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1281,10 +1281,10 @@ dependencies: "@types/node" "*" -"@types/chrome@^0.0.203": - version "0.0.203" - resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.203.tgz#cf883669ba489ec965e48d32cdcf14f45437f7cf" - integrity sha512-JlQNebwpBETVc8U1Rr2inDFuOTtn0lahRAhnddx1dd0S5RrLAFJEEsyIu7AXI14mkCgSunksnuLGioH8kvBqRA== +"@types/chrome@^0.0.204": + version "0.0.204" + resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.204.tgz#6125f5dbac7628e9f22370d25d63779bea3d64b0" + integrity sha512-EvnHfxMHUWP5EAlRMK66uIEUiy36t72vg5RwmzQv9tdIl2ZmAp92NwvmEZJKpbRnIMTEc2BmSmtrFiEISUJ0Sw== dependencies: "@types/filesystem" "*" "@types/har-format" "*" From 9cfa00c529bbf9cd9185f633d8e45b556f1978d2 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Tue, 13 Dec 2022 17:42:26 +0000 Subject: [PATCH 011/141] Update react-i18next to version 12.1.1 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8893204d23..572cfb850e 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "react": "^18.2.0", "react-confetti": "^6.1.0", "react-dom": "^18.2.0", - "react-i18next": "^12.0.0", + "react-i18next": "^12.1.1", "react-loading-skeleton": "^3.1.0", "react-modal": "^3.16.1", "react-qr-code": "^2.0.8", diff --git a/yarn.lock b/yarn.lock index 9017668061..3c0db3655e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7886,10 +7886,10 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" -react-i18next@^12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-12.0.0.tgz#634015a2c035779c5736ae4c2e5c34c1659753b1" - integrity sha512-/O7N6aIEAl1FaWZBNvhdIo9itvF/MO/nRKr9pYqRc9LhuC1u21SlfwpiYQqvaeNSEW3g3qUXLREOWMt+gxrWbg== +react-i18next@^12.1.1: + version "12.1.1" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-12.1.1.tgz#2626cdbfe6bcb76ef833861c0184a5c4e5e3c089" + integrity sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA== dependencies: "@babel/runtime" "^7.14.5" html-parse-stringify "^3.0.1" From 5dc6ce7b590da3d89b824861d6cc00b834f9b95a Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 14 Dec 2022 03:18:50 +0530 Subject: [PATCH 012/141] chore: disable analyzer mode in dev (#1861) --- webpack.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index d8e68c2dfa..c1f225c8b2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -205,8 +205,8 @@ var options = { patterns: [{ from: "static/assets", to: "assets" }], }), new BundleAnalyzerPlugin({ - generateStatsFile: true, - analyzerMode: 'static', + generateStatsFile: (nodeEnv !== "development" ? true : false), + analyzerMode: (nodeEnv !== "development" ? 'static' : 'disabled'), reportFilename: '../bundle-report.html', statsFilename: '../bundle-stats.json', openAnalyzer: nodeEnv !== "development", From 803ad8eb727edb98eee56f6e66ce25933800974b Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 14 Dec 2022 13:50:28 +0700 Subject: [PATCH 013/141] docs: advise against running multiple extensions --- doc/SETUP.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/SETUP.md b/doc/SETUP.md index 4b140a4dca..aa5faae322 100644 --- a/doc/SETUP.md +++ b/doc/SETUP.md @@ -26,6 +26,16 @@ To connect to a remote development LND node you can use a [test account](https://github.com/bumi/lightning-browser-extension/wiki/Test-setup) +### Multiple Extensions + +It is not recommended to have multiple versions of the extension (development + official) running in the same browser. You will have instances of the extension with the same icon which is confusing, and also leads to a poor webln experience as both extensions will launch a popup. There may also be unexpected bugs due to conflict with the two extensions running at the same time. + +Some ways you can work around this are: + +- Use a separate Chrome / firefox profile for development of the extension (this profile would not have the official extension installed) +- Use a dedicated browser for development of the extension (this browser would not have the official extension installed) +- Disable the official extension during development, and disable the development extension when you want to use Alby as normal. + ### Testnet/testing-accounts for development use localhost testnet For most people who are new to the btc lightning network, starting a test version of the lightning network environment locally is very helpful for developing wallets, so that they can transfer money with confidence. From eb74da831f10919ad4800cafebd01af2ad36dbd9 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 14 Dec 2022 13:59:25 +0700 Subject: [PATCH 014/141] chore: add vscode workspace settings --- .gitignore | 1 + .vscode/settings.json | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 37647cd508..872c35969d 100644 --- a/.gitignore +++ b/.gitignore @@ -144,6 +144,7 @@ dist/ .nova .vscode/**/* !.vscode/extensions.json +!.vscode/settings.json *.suo *.ntvs* *.njsproj diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..6fef09de22 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + }, + "typescript.preferences.importModuleSpecifier": "non-relative", + "editor.defaultFormatter": "esbenp.prettier-vscode" +} From 5ed6e2fd8dc174113af45d858e1aa6b881208988 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 14 Dec 2022 14:43:07 +0700 Subject: [PATCH 015/141] chore: add extra styling options to button --- src/app/components/Button/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/components/Button/index.tsx b/src/app/components/Button/index.tsx index aa683785a7..65cd967797 100644 --- a/src/app/components/Button/index.tsx +++ b/src/app/components/Button/index.tsx @@ -1,5 +1,5 @@ -import { forwardRef } from "react"; import type { Ref } from "react"; +import { forwardRef } from "react"; import Loading from "~/app/components/Loading"; import { classNames } from "~/app/utils/index"; @@ -9,8 +9,10 @@ export type Props = React.ButtonHTMLAttributes & { label: string; icon?: React.ReactNode; primary?: boolean; + outline?: boolean; loading?: boolean; disabled?: boolean; + flex?: boolean; direction?: "row" | "column"; }; @@ -26,7 +28,9 @@ const Button = forwardRef( fullWidth = false, halfWidth = false, primary = false, + outline = false, loading = false, + flex = false, }: Props, ref: Ref ) => { @@ -41,12 +45,15 @@ const Button = forwardRef( fullWidth || halfWidth ? "px-0 py-2" : "px-7 py-2", primary ? "bg-orange-bitcoin text-white border border-transparent" + : outline + ? "bg-white text-orange-bitcoin border border-orange-bitcoin" : `bg-white text-gray-700 dark:bg-surface-02dp dark:text-neutral-200 dark:border-neutral-800`, primary && !disabled && "hover:bg-orange-bitcoin-700", !primary && !disabled && "hover:bg-gray-50 dark:hover:bg-surface-16dp", disabled ? "cursor-default opacity-60" : "cursor-pointer", + flex && "flex-1", "inline-flex justify-center items-center font-medium rounded-md shadow focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-orange-bitcoin transition duration-150" )} onClick={onClick} From 3cf019e8111d629d024a75863793da2631a48273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=B6gel?= Date: Wed, 14 Dec 2022 12:04:13 +0100 Subject: [PATCH 016/141] + Navbar: properly center `children` ("Websites" etc. links) + let `.justify-between` do the work and don't define widths on the dropdown and hamburger menus. + add `.lg:-ml-2` to `children` since proper centering looks a bit too much on the left (it's a human issue :). --- src/app/components/Navbar/Navbar.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/components/Navbar/Navbar.tsx b/src/app/components/Navbar/Navbar.tsx index 731ec0401e..1cc13a154e 100644 --- a/src/app/components/Navbar/Navbar.tsx +++ b/src/app/components/Navbar/Navbar.tsx @@ -8,12 +8,14 @@ type Props = { export default function Navbar({ children }: Props) { return (
-
-
+
+
- {children && } -
+ + {children && } + +
From b0abd6ee1bbbff946b43d84195232f0a3ae6c5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=B6gel?= Date: Wed, 14 Dec 2022 12:09:13 +0100 Subject: [PATCH 017/141] remove superfluous wrapper divs. --- src/app/components/Navbar/Navbar.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/app/components/Navbar/Navbar.tsx b/src/app/components/Navbar/Navbar.tsx index 1cc13a154e..7e7168f0c9 100644 --- a/src/app/components/Navbar/Navbar.tsx +++ b/src/app/components/Navbar/Navbar.tsx @@ -9,15 +9,9 @@ export default function Navbar({ children }: Props) { return (
-
- -
- + {children && } - -
- -
+
); From a2c9ba2e11dc416cd78aa1e5522ee7536bcd250f Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Thu, 15 Dec 2022 00:56:25 +0200 Subject: [PATCH 018/141] feat: add request method functionality to lnd and commando (#1752) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add request method functionality to lnd and commando This implements the requestMethod for the lnd and commando connector exposing certain functions to webln. This allows web apps to do much more with lighthing than payments. * feat: add more supported cln methods * feat: add disconnect cln method * fix: removed release drafter workflow * chore: slightly try to cleanup the transaction list (#1751) * chore: slightly try to cleanup the transaction list * no need to display the type if all transactions are the same * preimage is not that important move it lower * properly show fees * fix: update tests * feat: improve transaction list (wip) * test(transactionsTable): update * refactor(transactionsTable): handle fee display * feat: cleanup * fix: strict comparison * fix: conditional display Co-authored-by: René Aaron Co-authored-by: escapedcat * Added translation using Weblate (Finnish) * Translated using Weblate (Finnish) Currently translated at 8.9% (35 of 392 strings) Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/fi/ * Translated using Weblate (Finnish) Currently translated at 72.1% (283 of 392 strings) Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/fi/ * Update @types/chrome to version 0.0.202 * Update filemanager-webpack-plugin to version 8.0.0 * Update webpack-cli to version 5.0.0 * Update tailwindcss to version 3.2.4 * Update all development Yarn dependencies (2022-11-19) * fix: correct language code for pt-BR * feat: show fiats formatted in locale * feat: format sats with Intl and locale * feat(paymentSummary): render formatted sats * feat(transactions): render formatted sats * feat(accountMenu): render and update formmated sats * feat(publisher): render formatted sats * feat(testConnection): render formatted sats * feat(lnurl): render formatted sats * feat(allowanceView): render formatted sats * feat(confirmpayment): render formatted sats * test: fallback to english locale * fix(allowanceView): correct helper * feat(publisher): render formatted sats * refactor: rename -> getFormattedFiat * refactor: rename -> getFormattedSats * refactor: move formatting funcs to context * test: wrap components in SettingsProvider * test: extend useSettings mock where needed * fix: add bkpr-listbalances * fix: improved permission dialog * fix: applied translation changes to all popups * feat(webln): expand getInfo call to return supported methods this makes webln.getInfo() similar to the webbtc spec and we return the supported methods of a connector. This helps the web app to decide if a connector supports all required methods. * feat(request): expand lnd methods * feat(webln requests): show method description if present * chore: lint * feat(request): extend LND/CLN request methods * feat(requests): add support for path parameters * test: update text * fix: translations for permissions * fix: updated translations & layout * fix: ts errors * fix: translations for lnd permissions * fix: remove description method from connectors Co-authored-by: kiwiidb Co-authored-by: René Aaron Co-authored-by: escapedcat Co-authored-by: Ricky Tigg Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> Co-authored-by: Lisa Oppermann Co-authored-by: René Aaron <100827540+reneaaron@users.noreply.github.com> --- .../ConfirmRequestPermission/index.tsx | 32 ++--- .../screens/ConfirmSignMessage/index.test.tsx | 2 +- src/app/screens/Enable/index.tsx | 2 +- src/app/screens/Nostr/ConfirmGetPublicKey.tsx | 2 +- .../background-script/actions/ln/getInfo.js | 3 + .../background-script/actions/ln/request.ts | 3 +- .../background-script/connectors/citadel.ts | 4 + .../background-script/connectors/commando.ts | 53 ++++++++ .../background-script/connectors/eclair.ts | 4 + .../background-script/connectors/galoy.ts | 4 + .../background-script/connectors/lnbits.ts | 4 + .../background-script/connectors/lnd.ts | 125 +++++++++++++++++- .../background-script/connectors/lndhub.ts | 4 + .../background-script/connectors/nativelnd.ts | 8 +- src/extension/ln/webbtc/index.ts | 14 +- src/i18n/i18nConfig.ts | 3 +- src/i18n/locales/en/translation.json | 68 ++++++++-- src/types.ts | 1 + 18 files changed, 291 insertions(+), 45 deletions(-) diff --git a/src/app/screens/ConfirmRequestPermission/index.tsx b/src/app/screens/ConfirmRequestPermission/index.tsx index eb24475350..286daa52ef 100644 --- a/src/app/screens/ConfirmRequestPermission/index.tsx +++ b/src/app/screens/ConfirmRequestPermission/index.tsx @@ -1,4 +1,3 @@ -import { CheckIcon } from "@bitcoin-design/bitcoin-icons-react/filled"; import ConfirmOrCancel from "@components/ConfirmOrCancel"; import Container from "@components/Container"; import PublisherCard from "@components/PublisherCard"; @@ -18,10 +17,12 @@ const ConfirmRequestPermission: FC = () => { keyPrefix: "confirm_request_permission", }); const { t: tCommon } = useTranslation("common"); + const { t: tPermissions } = useTranslation("permissions"); const navState = useNavigationState(); const origin = navState.origin as OriginData; const requestMethod = navState.args?.requestPermission?.method; + const description = navState.args?.requestPermission?.description; const enable = () => { msg.reply({ @@ -52,16 +53,20 @@ const ConfirmRequestPermission: FC = () => { url={origin.host} isSmall={false} /> - -
-

{t("allow", { host: origin.host })}

-
- -

- {t("enable_method", { method: requestMethod })} -

+
+

{t("allow")}

+
+

{requestMethod}

+ {description && ( +

+ {tPermissions( + description as unknown as TemplateStringsArray + )} +

+ )}
-
+ +
{ />
diff --git a/src/app/screens/ConfirmSignMessage/index.test.tsx b/src/app/screens/ConfirmSignMessage/index.test.tsx index c54eda6ad2..0ff240c8a3 100644 --- a/src/app/screens/ConfirmSignMessage/index.test.tsx +++ b/src/app/screens/ConfirmSignMessage/index.test.tsx @@ -45,7 +45,7 @@ describe("ConfirmSignMessage", () => { }); expect( - await screen.findByText("getalby.com asks you to sign:") + await screen.findByText("This website asks you to sign:") ).toBeInTheDocument(); expect(await screen.findByText("Test message")).toBeInTheDocument(); }); diff --git a/src/app/screens/Enable/index.tsx b/src/app/screens/Enable/index.tsx index 33b16e1a7e..5bc22ac699 100644 --- a/src/app/screens/Enable/index.tsx +++ b/src/app/screens/Enable/index.tsx @@ -87,7 +87,7 @@ function Enable(props: Props) { />
-

{t("allow", { host: props.origin.host })}

+

{t("allow")}

diff --git a/src/app/screens/Nostr/ConfirmGetPublicKey.tsx b/src/app/screens/Nostr/ConfirmGetPublicKey.tsx index 3450cc0f3f..75a2531e2d 100644 --- a/src/app/screens/Nostr/ConfirmGetPublicKey.tsx +++ b/src/app/screens/Nostr/ConfirmGetPublicKey.tsx @@ -63,7 +63,7 @@ function NostrConfirmGetPublicKey() { />
-

{t("allow", { host: origin.host })}

+

{t("allow")}

{t("read_public_key")}

diff --git a/src/extension/background-script/actions/ln/getInfo.js b/src/extension/background-script/actions/ln/getInfo.js index 3e3f9ae35d..42f9f16251 100644 --- a/src/extension/background-script/actions/ln/getInfo.js +++ b/src/extension/background-script/actions/ln/getInfo.js @@ -6,6 +6,9 @@ const getInfo = async (message, sender) => { return { data: { + version: "Alby", + supports: ["lightning"], + methods: connector.supportedMethods, node: { alias: info.data.alias, pubkey: info.data.pubkey, diff --git a/src/extension/background-script/actions/ln/request.ts b/src/extension/background-script/actions/ln/request.ts index 8f6e70a6f4..cc03f7853c 100644 --- a/src/extension/background-script/actions/ln/request.ts +++ b/src/extension/background-script/actions/ln/request.ts @@ -55,7 +55,8 @@ const request = async ( }>({ args: { requestPermission: { - method: method, + method, + description: `${connector.constructor.name.toLowerCase()}.${method}`, }, }, origin, diff --git a/src/extension/background-script/connectors/citadel.ts b/src/extension/background-script/connectors/citadel.ts index 490f4769d5..03e5dc01c6 100644 --- a/src/extension/background-script/connectors/citadel.ts +++ b/src/extension/background-script/connectors/citadel.ts @@ -48,6 +48,10 @@ class CitadelConnector implements Connector { return Promise.resolve(); } + get supportedMethods() { + return ["makeInvoice", "sendPayment", "signMessage", "getInfo"]; + } + async getInfo(): Promise { await this.ensureLogin(); return this.request("GET", "api/v1/lnd/info").then((data) => { diff --git a/src/extension/background-script/connectors/commando.ts b/src/extension/background-script/connectors/commando.ts index 5d9f3d2f47..c8db9622ae 100644 --- a/src/extension/background-script/connectors/commando.ts +++ b/src/extension/background-script/connectors/commando.ts @@ -78,6 +78,37 @@ type CommandoInvoice = { paid_at: number; payment_hash: string; }; + +const supportedMethods: string[] = [ + "bkpr-listbalances", + "checkmessage", + "connect", + "decode", + "decodepay", + "disconnect", + "feerates", + "fundchannel", + "getinfo", + "getroute", + "invoice", + "keysend", + "listforwards", + "listfunds", + "listinvoices", + "listnodes", + "listoffers", + "listpays", + "listpeers", + "listsendpays", + "listtransactions", + "multifundchannel", + "offer", + "pay", + "sendpay", + "setchannel", + "signmessage", +]; + export default class Commando implements Connector { config: Config; ln: LnMessage; @@ -107,6 +138,28 @@ export default class Commando implements Connector { await this.ln.disconnect(); } + get supportedMethods() { + return supportedMethods; + } + + async requestMethod( + method: string, + params: Record + ): Promise<{ data: unknown }> { + if (!this.supportedMethods.includes(method)) { + throw new Error(`${method} is not supported`); + } + const response = await this.ln.commando({ + method, + params, + rune: this.config.rune, + }); + + return { + data: response, + }; + } + async connectPeer(args: ConnectPeerArgs): Promise { return this.ln .commando({ diff --git a/src/extension/background-script/connectors/eclair.ts b/src/extension/background-script/connectors/eclair.ts index aa4f23ec96..07b9c6f9f9 100644 --- a/src/extension/background-script/connectors/eclair.ts +++ b/src/extension/background-script/connectors/eclair.ts @@ -37,6 +37,10 @@ class Eclair implements Connector { return Promise.resolve(); } + get supportedMethods() { + return ["getInfo", "keysend", "makeInvoice", "sendPayment", "signMessage"]; + } + getInfo(): Promise { return this.request("/getinfo", undefined).then((data) => { return { diff --git a/src/extension/background-script/connectors/galoy.ts b/src/extension/background-script/connectors/galoy.ts index 5e082651da..695f0258de 100644 --- a/src/extension/background-script/connectors/galoy.ts +++ b/src/extension/background-script/connectors/galoy.ts @@ -39,6 +39,10 @@ class Galoy implements Connector { return Promise.resolve(); } + get supportedMethods() { + return ["getInfo", "makeInvoice", "sendPayment", "signMessage"]; + } + getInfo(): Promise { const query = { query: ` diff --git a/src/extension/background-script/connectors/lnbits.ts b/src/extension/background-script/connectors/lnbits.ts index 57de068777..86ce896dd3 100644 --- a/src/extension/background-script/connectors/lnbits.ts +++ b/src/extension/background-script/connectors/lnbits.ts @@ -42,6 +42,10 @@ class LnBits implements Connector { return Promise.resolve(); } + get supportedMethods() { + return ["getInfo", "makeInvoice", "sendPayment", "signMessage"]; + } + getInfo(): Promise { return this.request( "GET", diff --git a/src/extension/background-script/connectors/lnd.ts b/src/extension/background-script/connectors/lnd.ts index 2e86292669..f3528afc6f 100644 --- a/src/extension/background-script/connectors/lnd.ts +++ b/src/extension/background-script/connectors/lnd.ts @@ -28,6 +28,104 @@ interface Config { url: string; } +const methods: Record> = { + getinfo: { + path: "/v1/getinfo", + httpMethod: "GET", + }, + listchannels: { + path: "/v1/channels", + httpMethod: "GET", + }, + listinvoices: { + path: "/v1/invoices", + httpMethod: "GET", + }, + channelbalance: { + path: "/v1/balance/channels", + httpMethod: "GET", + }, + walletbalance: { + path: "/v1/balance/blockchain", + httpMethod: "GET", + }, + openchannel: { + path: "/v1/channels", + httpMethod: "POST", + }, + connectpeer: { + path: "/v1/peers", + httpMethod: "POST", + }, + disconnectpeer: { + path: "/v1/peers/{{pub_key}}", + httpMethod: "DELETE", + }, + estimatefee: { + path: "/v1/transactions/fee", + httpMethod: "GET", + }, + getchaninfo: { + path: "/v1/graph/edge/{{chan_id}}", + httpMethod: "GET", + }, + getnetworkinfo: { + path: "/v1/graph/info", + httpMethod: "GET", + }, + getnodeinfo: { + path: "/v1/graph/node/{{pub_key}}", + httpMethod: "GET", + }, + gettransactions: { + path: "/v1/transactions", + httpMethod: "GET", + }, + listpayments: { + path: "/v1/payments", + httpMethod: "GET", + }, + listpeers: { + path: "/v1/peers", + httpMethod: "GET", + }, + lookupinvoice: { + path: "/v1/invoice/{{r_hash_str}}", + httpMethod: "GET", + }, + queryroutes: { + path: "/v1/graph/routes/{{pub_key}}/{{amt}}", + httpMethod: "GET", + }, + verifymessage: { + path: "/v1/verifymessage", + httpMethod: "POST", + }, + sendtoroute: { + path: "/v1/channels/transactions/route", + httpMethod: "POST", + }, + decodepayreq: { + path: "/v1/payreq/{{pay_req}}", + httpMethod: "GET", + }, +}; + +const pathTemplateParser = ( + template: string, + data: Record +): string => { + return template.replace(/{{(.*?)}}/g, (match) => { + const key = match.split(/{{|}}/).filter(Boolean)[0]; + const value = data[key]; + if (value === undefined) { + throw new Error(`Missing parameter ${key}`); + } + delete data[key]; + return String(value); // typecast to string + }); +}; + class Lnd implements Connector { config: Config; @@ -43,6 +141,31 @@ class Lnd implements Connector { return Promise.resolve(); } + get supportedMethods() { + return Object.keys(methods); + } + + async requestMethod( + method: string, + args: Record + ): Promise<{ data: unknown }> { + const methodDetails = methods[method]; + if (!methodDetails) { + throw new Error(`${method} is not supported`); + } + const httpMethod = methodDetails.httpMethod; + let path = methodDetails.path; + // add path parameters from the args hash and remove those attributes from args + // e.g. pathTemplateParser('invoice/{{r_hash_str}}', {r_hash_str: 'foo'}) + // will return invoice/foo and delete r_hash_str from the args object; + path = pathTemplateParser(path, args); + const response = await this.request(httpMethod, path, args); + + return { + data: response, + }; + } + getInfo(): Promise { return this.request<{ alias: string; @@ -313,7 +436,7 @@ class Lnd implements Connector { }; } - async request( + protected async request( method: string, path: string, args?: Record, diff --git a/src/extension/background-script/connectors/lndhub.ts b/src/extension/background-script/connectors/lndhub.ts index e9a38f2987..06c6cb690f 100644 --- a/src/extension/background-script/connectors/lndhub.ts +++ b/src/extension/background-script/connectors/lndhub.ts @@ -62,6 +62,10 @@ export default class LndHub implements Connector { return Promise.resolve(); } + get supportedMethods() { + return ["getInfo", "keysend", "makeInvoice", "sendPayment", "signMessage"]; + } + // not yet implemented async connectPeer(): Promise { console.error( diff --git a/src/extension/background-script/connectors/nativelnd.ts b/src/extension/background-script/connectors/nativelnd.ts index 804ca50f3c..1477179415 100644 --- a/src/extension/background-script/connectors/nativelnd.ts +++ b/src/extension/background-script/connectors/nativelnd.ts @@ -4,10 +4,10 @@ import Lnd from "./lnd"; const NativeConnector = Native(Lnd); export default class NativeLnd extends NativeConnector { - request( + protected request( method: string, path: string, - args?: Record + args?: Record ): Promise { const url = new URL(this.config.url); url.pathname = path; @@ -18,7 +18,9 @@ export default class NativeLnd extends NativeConnector { body = JSON.stringify(args) as string; headers["Content-Type"] = "application/json"; } else if (args !== undefined) { - url.search = new URLSearchParams(args).toString(); + url.search = new URLSearchParams( + args as Record + ).toString(); } if (this.config.macaroon) { headers["Grpc-Metadata-macaroon"] = this.config.macaroon; diff --git a/src/extension/ln/webbtc/index.ts b/src/extension/ln/webbtc/index.ts index 095a336c42..70e19add51 100644 --- a/src/extension/ln/webbtc/index.ts +++ b/src/extension/ln/webbtc/index.ts @@ -34,19 +34,7 @@ export default class WebBTCProvider { if (!this.enabled) { throw new Error("Provider must be enabled before calling getInfo"); } - return { - version: "stable", - supports: ["lightning"], - methods: [ - "enable", - "getInfo", - "signMessage", - "verifyMessage", - "makeInvoice", - "sendPayment", - "keysend", - ], - }; + return this.execute("getInfo"); } signMessage(message: string) { diff --git a/src/i18n/i18nConfig.ts b/src/i18n/i18nConfig.ts index 002daf2653..5d9f9e96a0 100644 --- a/src/i18n/i18nConfig.ts +++ b/src/i18n/i18nConfig.ts @@ -20,6 +20,7 @@ export const resources = { translation: en.translation, common: en.common, components: en.components, + permissions: en.permissions, }, es: { translation: es.translation, @@ -63,7 +64,7 @@ i18n .init({ //debug: true, fallbackLng: "en", - ns: ["translation", "common", "components"], + ns: ["translation", "common", "components", "permissions"], defaultNS, resources, }); diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 44495a3e42..4dd3896aad 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -375,7 +375,7 @@ }, "enable": { "title": "Connect", - "allow": "Allow {{host}} to:", + "allow": "Allow this website to:", "request1": "Request approval for transactions", "request2": "Request invoices and lightning information", "block_and_ignore": "Block and ignore {{host}}", @@ -607,7 +607,7 @@ }, "confirm_sign_message": { "title": "Sign", - "content": "{{host}} asks you to sign:" + "content": "This website asks you to sign:" }, "confirm_keysend": { "title": "Approve Payment", @@ -635,20 +635,19 @@ }, "confirm_request_permission": { "title": "Approve Request", - "allow": "Allow {{host}} to:", - "enable_method": "Call {{method}}", - "always_allow": "Always allow {{method}} on {{host}}" + "allow": "Allow this website to execute:", + "always_allow": "Remember my choice and don't ask again" }, "nostr": { "title": "Nostr", - "content": "{{host}} asks you to sign:", - "allow": "Allow {{host}} to:", + "allow": "Allow this website to:", + "content": "This website asks you to sign:", "read_public_key": "Read your public key", "block_and_ignore": "Block and ignore {{host}}", "block_added": "Added {{host}} to the blocklist, please reload the website.", "confirm_sign_message": { "remember": { - "label": "Remember and don't ask again" + "label": "Remember my choice and don't ask again" } }, "permissions": { @@ -794,5 +793,58 @@ "auth": "LOGIN" } } + }, + "permissions": { + "commando": { + "bkpr-listbalances": "List of all current and historical account balances.", + "checkmessage": "Verify that the signature was generated by a given node.", + "connect": "Establish a new connection with another node.", + "decode": "Decode a bolt11/bolt12/rune string.", + "decodepay": "Check and parse a bolt11 string.", + "disconnect": "Close an existing connection to a peer.", + "feerates": "Return the feerates that CLN will use.", + "fundchannel": "Open a payment channel with a peer by committing a funding transaction.", + "getinfo": "Get the summary of the node.", + "getroute": "Find the best route for the payment to a lightning node.", + "invoice": "Create the expectation of a payment.", + "keysend": "Send a payment to another node.", + "listforwards": "List all htlcs that have been attempted to be forwarded.", + "listfunds": "List all funds available.", + "listinvoices": "Get the status of all invoices.", + "listnodes": "List nodes the node has learned about via gossip messages.", + "listoffers": "List all offers or get a specific offer.", + "listpays": "Gets the status of all pay commands.", + "listpeers": "List nodes that are connected or have open channels with this node.", + "listsendpays": "Gets the status of all sendpay commands.", + "listtransactions": "List transactions tracked in the wallet.", + "multifundchannel": "Open multiple payment channels with nodes by committing a single funding transaction.", + "offer": "Create an offer.", + "pay": "Send a payment to a BOLT11 invoice.", + "sendpay": "Send a payment via a route.", + "setchannel": "Configure fees / htlc range advertized for a channel.", + "signmessage": "Create a signature from this node." + }, + "lnd": { + "getinfo": "Get the node information.", + "listchannels": "Get a description of all the open channels.", + "listinvoices": "Get a list of all invoices.", + "channelbalance": "Get a report on the total funds across all open channels.", + "walletbalance": "Get the total unspent outputs of the wallet.", + "openchannel": "Open a new channel.", + "connectpeer": "Establish a connection to a remote peer.", + "disconnectpeer": "Disconnect from a remote peer.", + "estimatefee": "Estimate the fee rate and total fees for a transaction.", + "getchaninfo": "Get the network announcement for the given channel.", + "getnetworkinfo": "Get basic stats about the known channel graph.", + "getnodeinfo": "Get the channel information for a node.", + "gettransactions": "Get a list of all transactions relevant to the wallet.", + "listpayments": "list of all outgoing payments.", + "listpeers": "list all currently active peers.", + "lookupinvoice": "Look up invoice details.", + "queryroutes": "Query for a possible route.", + "verifymessage": "Verify a signature over a msg.", + "sendtoroute": "Make a payment via the specified route.", + "decodepayreq": "Decode a payment request string." + } } } diff --git a/src/types.ts b/src/types.ts index ff1601370a..adfce13f06 100644 --- a/src/types.ts +++ b/src/types.ts @@ -144,6 +144,7 @@ export type NavigationState = { details?: string; requestPermission: { method: string; + description: string; }; }; isPrompt?: true; // only passed via Prompt.tsx From f04cb0477422e95579b0a264682ff8a49a436d1c Mon Sep 17 00:00:00 2001 From: escapedcat Date: Wed, 14 Dec 2022 12:53:05 +0000 Subject: [PATCH 019/141] Translated using Weblate (German) Currently translated at 48.7% (196 of 402 strings) Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/de/ --- src/i18n/locales/de/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 6dda99bd70..258ad1a047 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -1,7 +1,7 @@ { "translation": { "welcome": { - "title": "Die Macht von ⚡ Bitcoin ⚡ in Ihrem Browser", + "title": "Die Macht von ⚡ bitcoin ⚡ in deinem Browser", "nav": { "welcome": "Willkommen", "password": "Dein Passwort", From 295224320dbb468f85ccba92c31bd71a185d8384 Mon Sep 17 00:00:00 2001 From: zhangchao <544262408@qq.com> Date: Thu, 15 Dec 2022 13:34:52 +0800 Subject: [PATCH 020/141] fix: lnurl auth return value cause error msg --- src/extension/background-script/actions/lnurl/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/background-script/actions/lnurl/auth.ts b/src/extension/background-script/actions/lnurl/auth.ts index 92e2762560..407c22b890 100644 --- a/src/extension/background-script/actions/lnurl/auth.ts +++ b/src/extension/background-script/actions/lnurl/auth.ts @@ -89,7 +89,7 @@ export async function authFunction({ ); // if the service returned with a HTTP 200 we still check if the response data is OK - if (authResponse?.data.status.toUpperCase() !== "OK") { + if (authResponse?.data.status?.toUpperCase() !== "OK") { throw new Error( authResponse?.data?.reason || "Auth: Something went wrong" ); From dcf7a0915accd10cbaf21467fb629e4de9d6e0fa Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 15 Dec 2022 13:36:06 +0700 Subject: [PATCH 021/141] chore: remove alby wallet from connectors --- src/app/router/connectorRoutes.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/app/router/connectorRoutes.tsx b/src/app/router/connectorRoutes.tsx index 25a5edd110..65b6de2182 100644 --- a/src/app/router/connectorRoutes.tsx +++ b/src/app/router/connectorRoutes.tsx @@ -9,11 +9,9 @@ import ConnectMyNode from "@screens/connectors/ConnectMyNode"; import ConnectRaspiBlitz from "@screens/connectors/ConnectRaspiBlitz"; import ConnectStart9 from "@screens/connectors/ConnectStart9"; import ConnectUmbrel from "@screens/connectors/ConnectUmbrel"; -import NewWallet from "@screens/connectors/NewWallet"; import i18n from "~/i18n/i18nConfig"; import ConnectCommando from "../screens/connectors/ConnectCommando"; -import alby from "/static/assets/icons/alby.png"; import btcpay from "/static/assets/icons/btcpay.svg"; import citadel from "/static/assets/icons/citadel.png"; import core_ln from "/static/assets/icons/core_ln.svg"; @@ -36,13 +34,6 @@ const galoyPaths: { [key: string]: keyof typeof galoyUrls } = { function getConnectorRoutes() { return [ - { - path: "create-wallet", - element: , - title: i18n.t("translation:choose_connector.alby.title"), - description: i18n.t("translation:choose_connector.alby.description"), - logo: alby, - }, { path: "lnd", element: , From a021a25b9c6cc9e60a8ac6783ab1d96a1e8a6e9d Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 15 Dec 2022 15:36:19 +0700 Subject: [PATCH 022/141] feat: split account add screen --- src/app/components/ConnectorPath/index.tsx | 21 +++++ src/app/router/Options/Options.tsx | 39 ++++++--- src/app/router/Welcome/Welcome.tsx | 43 +++++++--- src/app/screens/Onboard/SetPassword/index.tsx | 2 +- .../connectors/ChooseConnectorPath/index.tsx | 86 +++++++++++++++++++ .../screens/connectors/NewWallet/index.tsx | 2 +- src/i18n/locales/en/translation.json | 80 ++++++++++------- 7 files changed, 219 insertions(+), 54 deletions(-) create mode 100644 src/app/components/ConnectorPath/index.tsx create mode 100644 src/app/screens/connectors/ChooseConnectorPath/index.tsx diff --git a/src/app/components/ConnectorPath/index.tsx b/src/app/components/ConnectorPath/index.tsx new file mode 100644 index 0000000000..1301fd7a69 --- /dev/null +++ b/src/app/components/ConnectorPath/index.tsx @@ -0,0 +1,21 @@ +type Props = { + title: string; + description: string; + content: React.ReactNode; + actions: React.ReactNode; +}; + +function ConnectorPath({ title, description, content, actions }: Props) { + return ( +
+

{title}

+

{description}

+
+ {content} +
+
{actions}
+
+ ); +} + +export default ConnectorPath; diff --git a/src/app/router/Options/Options.tsx b/src/app/router/Options/Options.tsx index 73a6df3870..58e7e05014 100644 --- a/src/app/router/Options/Options.tsx +++ b/src/app/router/Options/Options.tsx @@ -14,13 +14,15 @@ import Receive from "@screens/Receive"; import Send from "@screens/Send"; import Settings from "@screens/Settings"; import Unlock from "@screens/Unlock"; -import ChooseConnector from "@screens/connectors/ChooseConnector"; import { useTranslation } from "react-i18next"; -import { HashRouter, Navigate, Outlet, Routes, Route } from "react-router-dom"; +import { HashRouter, Navigate, Outlet, Route, Routes } from "react-router-dom"; import { ToastContainer } from "react-toastify"; import Providers from "~/app/context/Providers"; import RequireAuth from "~/app/router/RequireAuth"; import getConnectorRoutes from "~/app/router/connectorRoutes"; +import ChooseConnector from "~/app/screens/connectors/ChooseConnector"; +import ChooseConnectorPath from "~/app/screens/connectors/ChooseConnectorPath"; +import NewWallet from "~/app/screens/connectors/NewWallet"; import i18n from "~/i18n/i18nConfig"; function Options() { @@ -56,7 +58,7 @@ function Options() { + } @@ -64,20 +66,35 @@ function Options() { } /> - {connectorRoutes.map((connectorRoute) => ( + } /> + + } /> - ))} + {connectorRoutes.map((connectorRoute) => ( + + ))} + } /> diff --git a/src/app/router/Welcome/Welcome.tsx b/src/app/router/Welcome/Welcome.tsx index 87dcde774c..f3d88cfb15 100644 --- a/src/app/router/Welcome/Welcome.tsx +++ b/src/app/router/Welcome/Welcome.tsx @@ -3,13 +3,16 @@ import Steps from "@components/Steps"; import Intro from "@screens/Onboard/Intro"; import SetPassword from "@screens/Onboard/SetPassword"; import TestConnection from "@screens/Onboard/TestConnection"; -import ChooseConnector from "@screens/connectors/ChooseConnector"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { HashRouter as Router, useRoutes, useLocation } from "react-router-dom"; +import { HashRouter as Router, useLocation, useRoutes } from "react-router-dom"; import { ToastContainer } from "react-toastify"; +import Container from "~/app/components/Container"; import { SettingsProvider } from "~/app/context/SettingsContext"; import getConnectorRoutes from "~/app/router/connectorRoutes"; +import ChooseConnector from "~/app/screens/connectors/ChooseConnector"; +import ChooseConnectorPath from "~/app/screens/connectors/ChooseConnectorPath"; +import NewWallet from "~/app/screens/connectors/NewWallet"; import i18n from "~/i18n/i18nConfig"; let connectorRoutes = getConnectorRoutes(); @@ -35,19 +38,41 @@ function getRoutes( name: i18n.t("translation:welcome.nav.password"), }, { - path: "/choose-connector", + path: "/choose-path", name: i18n.t("translation:welcome.nav.connect"), children: [ { index: true, element: ( - ), }, - ...connectorRoutes, + { + path: "create-wallet", + element: , + }, + { + path: "choose-connector", + children: [ + { + index: true, + element: ( + + ), + }, + ...connectorRoutes, + ], + }, ], }, { @@ -133,9 +158,7 @@ function App() {
-
- {routesElement} -
+ {routesElement}
); } diff --git a/src/app/screens/Onboard/SetPassword/index.tsx b/src/app/screens/Onboard/SetPassword/index.tsx index 55e60f5f4f..7335c0dcaa 100644 --- a/src/app/screens/Onboard/SetPassword/index.tsx +++ b/src/app/screens/Onboard/SetPassword/index.tsx @@ -23,7 +23,7 @@ export default function SetPassword() { event.preventDefault(); try { await msg.request("setPassword", { password: formData.password }); - navigate("/choose-connector"); + navigate("/choose-path"); } catch (e) { if (e instanceof Error) { console.error(e.message); diff --git a/src/app/screens/connectors/ChooseConnectorPath/index.tsx b/src/app/screens/connectors/ChooseConnectorPath/index.tsx new file mode 100644 index 0000000000..4e50d2b10f --- /dev/null +++ b/src/app/screens/connectors/ChooseConnectorPath/index.tsx @@ -0,0 +1,86 @@ +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import Button from "~/app/components/Button"; +import ConnectorPath from "~/app/components/ConnectorPath"; +import getConnectorRoutes from "~/app/router/connectorRoutes"; +import i18n from "~/i18n/i18nConfig"; + +import alby from "/static/assets/icons/alby.png"; + +type Props = { + title: string; + description?: string; +}; + +export default function ChooseConnectorPath({ title, description }: Props) { + let connectorRoutes = getConnectorRoutes(); + i18n.on("languageChanged", () => { + connectorRoutes = getConnectorRoutes(); + }); + const { t } = useTranslation("translation", { + keyPrefix: "choose_path", + }); + const { t: tCommon } = useTranslation("common"); + return ( +
+
+
+

{title}

+ {description && ( +
+

+ {description} +

+
+ )} +
+ +
+ + } + actions={ + <> + +
+ } + actions={ + +
+
+
+ ); +} diff --git a/src/app/screens/connectors/NewWallet/index.tsx b/src/app/screens/connectors/NewWallet/index.tsx index 25635f4157..71d260cec7 100644 --- a/src/app/screens/connectors/NewWallet/index.tsx +++ b/src/app/screens/connectors/NewWallet/index.tsx @@ -33,7 +33,7 @@ export default function NewWallet() { const [loading, setLoading] = useState(false); const navigate = useNavigate(); const { t } = useTranslation("translation", { - keyPrefix: "choose_connector.alby", + keyPrefix: "alby", }); const { t: tCommon } = useTranslation("common"); diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 44495a3e42..c7e5765a07 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -5,7 +5,7 @@ "nav": { "welcome": "Welcome", "password": "Your Password", - "connect": "Your Lightning account", + "connect": "Create or Connect Wallet", "done": "Done" }, "intro": { @@ -44,6 +44,9 @@ "mismatched_password": "Passwords don't match." } }, + "choose_path": { + "description": "Start by creating a new Alby Wallet, logging in to an existing one or connecting lightning wallet. You will be able to connect and manage more wallets later as well!" + }, "test_connection": { "ready": "Awesome, you’re ready to go!", "tutorial": "Now you’ve connected your wallet would you like to go through a tutorial?", @@ -58,38 +61,52 @@ } } }, - "choose_connector": { - "title": { - "welcome": "Do you have a lightning wallet?", - "options": "Add a new lightning account" - }, - "description": "You need to first connect to a lightning wallet so that you can interact with your favorite websites that accept bitcoin lightning payments!", + "choose_path": { + "title": "Create or Connect Wallet", + "description": "Create a new Alby Wallet, log in to an existing one or connect to an external wallet or node", "alby": { "title": "Alby Wallet", - "description": "Create or login to your Alby account", - "pre_connect": { - "title": "Your Alby Lightning Wallet", - "login_account": "Create or login to your Alby account.", - "host_wallet": "We host a Lightning wallet for you!", - "email": { - "label": "Email Address" - }, - "optional_lightning_note": { - "part1": "Your Alby account also comes with an optional", - "part2": "Lightning Address", - "part3": ". This is a simple way for anyone to send you Bitcoin on the Lightning Network.", - "part4": "learn more" - }, - "optional_lightning_address": { - "label": "Choose your Lightning Address (optional)", - "suffix": "@getalby.com", - "title": "numbers and letters, at least 3 characters" - }, - "errors": { - "create_wallet_error": "Failed to login or create a new account. If you need help, please contact support@getalby.com" - } + "description": "Create or log in to your Alby Wallet", + "create_new": "Create new" + }, + "other": { + "title": "Other Wallets", + "description": "Connect to your external lightning wallet or node", + "and_more": "and more...", + "connect": "Connect to Lightning Wallet" + } + }, + "alby": { + "create_wallet": { + "title": "Alby Wallet", + "description": "Create or login to your Alby account" + }, + "pre_connect": { + "title": "Your Alby Lightning Wallet", + "login_account": "Create or login to your Alby account.", + "host_wallet": "We host a Lightning wallet for you!", + "email": { + "label": "Email Address" + }, + "optional_lightning_note": { + "part1": "Your Alby account also comes with an optional", + "part2": "Lightning Address", + "part3": ". This is a simple way for anyone to send you Bitcoin on the Lightning Network.", + "part4": "learn more" + }, + "optional_lightning_address": { + "label": "Choose your Lightning Address (optional)", + "suffix": "@getalby.com", + "title": "numbers and letters, at least 3 characters" + }, + "errors": { + "create_wallet_error": "Failed to login or create a new account. If you need help, please contact support@getalby.com" } - }, + } + }, + "choose_connector": { + "title": "Connect Lightning Wallet", + "description": "Connect to your external lightning wallet or node", "lnd": { "title": "LND", "description": "Connect to your LND node", @@ -696,7 +713,8 @@ "close": "Close", "export": "Export", "remove": "Remove", - "copy": "Copy" + "copy": "Copy", + "log_in": "Log in" }, "errors": { "connection_failed": "Connection failed", From 480250f25186fd7fc0c05e16e7f0692da42cbe9f Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 15 Dec 2022 15:56:36 +0700 Subject: [PATCH 023/141] chore: fix e2e tests --- src/i18n/locales/en/translation.json | 2 +- tests/e2e/001-createWallets.spec.ts | 25 +++++++++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index c7e5765a07..19c514d29f 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -5,7 +5,7 @@ "nav": { "welcome": "Welcome", "password": "Your Password", - "connect": "Create or Connect Wallet", + "connect": "Your Lightning account", "done": "Done" }, "intro": { diff --git a/tests/e2e/001-createWallets.spec.ts b/tests/e2e/001-createWallets.spec.ts index 328f02b623..b7d821ab89 100644 --- a/tests/e2e/001-createWallets.spec.ts +++ b/tests/e2e/001-createWallets.spec.ts @@ -8,7 +8,9 @@ const { getByText, getByLabelText, findByLabelText, findByText } = queries; const user = USER.SINGLE(); -const commonCreateWalletUserCreate = async () => { +const commonCreateWalletUserCreate = async ( + connectToLightningWallet = true +) => { const { page, browser } = await loadExtension(); // get document from welcome page @@ -46,7 +48,22 @@ const commonCreateWalletUserCreate = async () => { page.waitForNavigation(), // The promise resolves after navigation has finished ]); - await findByText($document, "Do you have a lightning wallet?"); + await findByText($document, "Create or Connect Wallet"); + + if (connectToLightningWallet) { + const chooseConnectorButton = await findByText( + $document, + "Connect to Lightning Wallet" + ); + chooseConnectorButton.click(); + + await Promise.all([ + page.waitForResponse(() => true), + page.waitForNavigation(), // The promise resolves after navigation has finished + ]); + + await findByText($document, "Connect Lightning Wallet"); + } return { user, browser, page, $document }; }; @@ -67,10 +84,10 @@ const commonCreateWalletSuccessCheck = async ({ page, $document }) => { test.describe("Create or connect wallets", () => { test("successfully creates an Alby wallet", async () => { const { user, browser, page, $document } = - await commonCreateWalletUserCreate(); + await commonCreateWalletUserCreate(false); // click at "Create Alby Wallet" - const createNewWalletButton = await getByText($document, "Alby Wallet"); + const createNewWalletButton = await getByText($document, "Create new"); createNewWalletButton.click(); await findByText($document, "Your Alby Lightning Wallet"); From 14ce0eb94c7bb9897c6958e98a23e6c2e69f5a77 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 15 Dec 2022 16:23:38 +0700 Subject: [PATCH 024/141] chore: minor cleanup --- src/app/screens/connectors/ChooseConnector/index.tsx | 2 +- src/app/screens/connectors/ChooseConnectorPath/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/screens/connectors/ChooseConnector/index.tsx b/src/app/screens/connectors/ChooseConnector/index.tsx index cbc78ec83e..6e1425780a 100644 --- a/src/app/screens/connectors/ChooseConnector/index.tsx +++ b/src/app/screens/connectors/ChooseConnector/index.tsx @@ -13,7 +13,7 @@ export default function ChooseConnector({ title, description }: Props) { connectorRoutes = getConnectorRoutes(); }); return ( -
+

{title}

diff --git a/src/app/screens/connectors/ChooseConnectorPath/index.tsx b/src/app/screens/connectors/ChooseConnectorPath/index.tsx index 4e50d2b10f..84e83cfb0b 100644 --- a/src/app/screens/connectors/ChooseConnectorPath/index.tsx +++ b/src/app/screens/connectors/ChooseConnectorPath/index.tsx @@ -9,7 +9,7 @@ import alby from "/static/assets/icons/alby.png"; type Props = { title: string; - description?: string; + description: string; }; export default function ChooseConnectorPath({ title, description }: Props) { From 5f09b5f7068b8538fe748cb7993b9c483b4fe160 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 15 Dec 2022 16:52:50 +0700 Subject: [PATCH 025/141] docs: add more details to contributon document --- doc/CONTRIBUTION.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/CONTRIBUTION.md b/doc/CONTRIBUTION.md index 5e5040500d..d2a7dbb477 100644 --- a/doc/CONTRIBUTION.md +++ b/doc/CONTRIBUTION.md @@ -30,7 +30,7 @@ When creating a PR please take this points as a reminder: - Think in iterations (babysteps)\ You can always start a PR and if you feel like adding on more things to it, better branch off and [create a new i.e. _draft_-PR](https://github.blog/2019-02-14-introducing-draft-pull-requests/) - Newly added components should have a unit-test -- If you work on a more complex PR please [join our community chat](https://bitcoindesign.slack.com/archives/C02591ADXM2) to get feedback, discuss the best way to tackle the challenge, and to ensure that there's no duplication of work. It's often faster and nicer to chat or call about questions than to do ping-pong comments in PRs +- If you work on a more complex PR please [join our community chat](https://bitcoindesign.slack.com/archives/C02591ADXM2) (Invite link at https://bitcoin.design/) to get feedback, discuss the best way to tackle the challenge, and to ensure that there's no duplication of work. It's often faster and nicer to chat or call about questions than to do ping-pong comments in PRs ### Code format & preferences @@ -49,6 +49,10 @@ For better support we recommend these extensions: - [vscode-tailwindcss](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) - [vscode-html-css](https://marketplace.visualstudio.com/items?itemName=ecmel.vscode-html-css) +### Branch names + +Please prefix your branch names with `feature/`, `fix/`, `chore/`, `refactor/`, `docs/` based on the intent of the branch or issue being addressed (see commit message format below). + ### Commit message format Alby enforces [Conventional Commits Specification](https://www.conventionalcommits.org/en/) From 8b61c4e50ce084e705ebd7abf7f9a8bcd43d46c4 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Thu, 15 Dec 2022 17:16:19 +0700 Subject: [PATCH 026/141] docs: rename feature/ to feat/ --- doc/CONTRIBUTION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CONTRIBUTION.md b/doc/CONTRIBUTION.md index d2a7dbb477..fe4d9afd7a 100644 --- a/doc/CONTRIBUTION.md +++ b/doc/CONTRIBUTION.md @@ -51,7 +51,7 @@ For better support we recommend these extensions: ### Branch names -Please prefix your branch names with `feature/`, `fix/`, `chore/`, `refactor/`, `docs/` based on the intent of the branch or issue being addressed (see commit message format below). +Please prefix your branch names with `feat/`, `fix/`, `chore/`, `refactor/`, `docs/` based on the intent of the branch or issue being addressed (see commit message format below). ### Commit message format From 6082005d546dc12634e78f38e92e13670470e806 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 15 Dec 2022 18:30:40 +0700 Subject: [PATCH 027/141] chore: remove description from each entry on connector screen --- src/app/components/LinkButton/index.tsx | 14 +++++------- src/app/router/Welcome/Welcome.tsx | 1 - src/app/router/connectorRoutes.tsx | 22 ------------------- .../connectors/ChooseConnector/index.tsx | 10 ++------- src/i18n/locales/en/translation.json | 14 ------------ 5 files changed, 7 insertions(+), 54 deletions(-) diff --git a/src/app/components/LinkButton/index.tsx b/src/app/components/LinkButton/index.tsx index 7031444ceb..eeea65f5ca 100644 --- a/src/app/components/LinkButton/index.tsx +++ b/src/app/components/LinkButton/index.tsx @@ -3,24 +3,20 @@ import { Link } from "react-router-dom"; type Props = { to: string; title: string; - description?: string; logo?: string; }; -export default function LinkButton({ to, title, description, logo }: Props) { +export default function LinkButton({ to, title, logo }: Props) { return ( -
+
logo
- {title} - {description && ( - - {description} - - )} + + {title} +
diff --git a/src/app/router/Welcome/Welcome.tsx b/src/app/router/Welcome/Welcome.tsx index f3d88cfb15..cba91f3a50 100644 --- a/src/app/router/Welcome/Welcome.tsx +++ b/src/app/router/Welcome/Welcome.tsx @@ -22,7 +22,6 @@ function getRoutes( path: string; element: JSX.Element; title: string; - description: string; logo: string; }[] ) { diff --git a/src/app/router/connectorRoutes.tsx b/src/app/router/connectorRoutes.tsx index 65b6de2182..e37fd1795e 100644 --- a/src/app/router/connectorRoutes.tsx +++ b/src/app/router/connectorRoutes.tsx @@ -38,106 +38,84 @@ function getConnectorRoutes() { path: "lnd", element: , title: i18n.t("translation:choose_connector.lnd.title"), - description: i18n.t("translation:choose_connector.lnd.description"), logo: lnd, }, { path: "commando", element: , title: i18n.t("translation:choose_connector.commando.title"), - description: i18n.t("translation:choose_connector.commando.description"), logo: core_ln, }, { path: "lnbits", element: , title: i18n.t("translation:choose_connector.lnbits.title"), - description: i18n.t("translation:choose_connector.lnbits.description"), logo: lnbits, }, { path: "lnd-hub-go", element: , title: i18n.t("translation:choose_connector.lndhub_go.title"), - description: i18n.t("translation:choose_connector.lndhub_go.description"), logo: lndhubGo, }, { path: "lnd-hub-bluewallet", element: , title: i18n.t("translation:choose_connector.lndhub_bluewallet.title"), - description: i18n.t( - "translation:choose_connector.lndhub_bluewallet.description" - ), logo: lndhubBlueWallet, }, { path: "eclair", element: , title: i18n.t("translation:choose_connector.eclair.title"), - description: i18n.t("translation:choose_connector.eclair.description"), logo: eclair, }, { path: "citadel", element: , title: i18n.t("translation:choose_connector.citadel.title"), - description: i18n.t("translation:choose_connector.citadel.description"), logo: citadel, }, { path: "umbrel", element: , title: i18n.t("translation:choose_connector.umbrel.title"), - description: i18n.t("translation:choose_connector.umbrel.description"), logo: umbrel, }, { path: "mynode", element: , title: i18n.t("translation:choose_connector.mynode.title"), - description: i18n.t("translation:choose_connector.mynode.description"), logo: mynode, }, { path: "start9", element: , title: i18n.t("translation:choose_connector.start9.title"), - description: i18n.t("translation:choose_connector.start9.description"), logo: start9, }, { path: "raspiblitz", element: , title: i18n.t("translation:choose_connector.raspiblitz.title"), - description: i18n.t( - "translation:choose_connector.raspiblitz.description" - ), logo: raspiblitz, }, { path: galoyPaths.bitcoinBeach, element: , title: i18n.t("translation:choose_connector.bitcoin_beach.title"), - description: i18n.t( - "translation:choose_connector.bitcoin_beach.description" - ), logo: galoyBitcoinBeach, }, { path: galoyPaths.bitcoinJungle, element: , title: i18n.t("translation:choose_connector.bitcoin_jungle.title"), - description: i18n.t( - "translation:choose_connector.bitcoin_jungle.description" - ), logo: galoyBitcoinJungle, }, { path: "btcpay", element: , title: i18n.t("translation:choose_connector.btcpay.title"), - description: i18n.t("translation:choose_connector.btcpay.description"), logo: btcpay, }, ]; diff --git a/src/app/screens/connectors/ChooseConnector/index.tsx b/src/app/screens/connectors/ChooseConnector/index.tsx index 6e1425780a..38c2a3819e 100644 --- a/src/app/screens/connectors/ChooseConnector/index.tsx +++ b/src/app/screens/connectors/ChooseConnector/index.tsx @@ -24,14 +24,8 @@ export default function ChooseConnector({ title, description }: Props) { )}
- {connectorRoutes.map(({ path, title, description, logo }) => ( - + {connectorRoutes.map(({ path, title, logo }) => ( + ))}
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 870ec3a1f5..52a6f39cf8 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -109,7 +109,6 @@ "description": "Connect to your external lightning wallet or node", "lnd": { "title": "LND", - "description": "Connect to your LND node", "page": { "title": "Connect to your LND node", "description": "You need your node URL and a macaroon with read and send permissions (e.g. admin.macaroon)" @@ -129,7 +128,6 @@ }, "lndhub_bluewallet": { "title": "Bluewallet", - "description": "Connect to your Bluewallet mobile wallet", "page": { "title": "Connect to BlueWallet", "description": "In BlueWallet, choose the wallet you want to connect, open it, click on \"...\", click on Export/Backup to display the QR code and scan it with your webcam." @@ -144,7 +142,6 @@ }, "lndhub_go": { "title": "LNDHub", - "description": "Connect to your LNDHub account", "page": { "title": "Connect to LNDHub", "description": "Input your LNDHub credential URI here or scan the QR code with your webcam." @@ -159,7 +156,6 @@ }, "lnbits": { "title": "LNbits", - "description": "Connect to your LNbits account", "page": { "title": "Connect to <0>LNbits", "instructions": "In LNbits, choose the wallet you want to connect, open it, click on API Info and copy the Admin Key. Paste it below:" @@ -177,7 +173,6 @@ }, "eclair": { "title": "Eclair", - "description": "Connect to your Eclair node", "page": { "title": "Connect to <0>Eclair", "instructions": "You need your Eclair URL and password." @@ -192,7 +187,6 @@ }, "citadel": { "title": "Citadel", - "description": "Connect to your local Citadel", "page": { "title": "Connect to <0>Citadel node", "instructions": "This currently doesn't work if 2FA is enabled." @@ -207,7 +201,6 @@ }, "umbrel": { "title": "Umbrel", - "description": "Connect to your Umbrel", "page": { "title": "Connect to <0>Umbrel node", "instructions": "In your Umbrel dashboard go to <0>Connect Wallet. Select <0>lndconnect REST and copy the <0>lndconnect URL. (Depending on your setup you can either use the local connection or the Tor connection.)" @@ -219,7 +212,6 @@ }, "mynode": { "title": "myNode", - "description": "Connect to your myNode", "page": { "title": "Connect to <0>myNode", "instructions": "On your myNode homepage click on the <0>Wallet button for your <0>Lightning service.<1/> Now click on the <0>Pair Wallet button under the <0>Status tab. Enter your password when prompted.<1/> Select the dropdown menu and choose a pairing option. Depending on your setup you can either use the <0>Lightning (REST + Local IP) connection or the <0>Lightning (REST + Tor) connection." @@ -231,7 +223,6 @@ }, "start9": { "title": "Start9", - "description": "Connect to your Embassy", "page": { "title": "Connect to your <0>Embassy node", "instructions": "<0>Note: Currently we only support LND but we will be adding c-lightning support in the future!<1/>On your Embassy dashboard click on the <0>Lightning Network Daemon service.<1/>Select the <0>Properties tab.<1/>Now copy the <0>LND Connect REST URL." @@ -243,7 +234,6 @@ }, "raspiblitz": { "title": "RaspiBlitz", - "description": "Connect to your RaspiBlitz", "page": { "title": "Connect to your <0>RaspiBlitz node", "instructions1": "You need your node onion address, port, and a macaroon with read and send permissions (e.g. admin.macaroon).<1/><1/><0>SSH into your <0>RaspiBlitz.<1/>Run the command <0>sudo cat /mnt/hdd/tor/lndrest/hostname.<1/>Copy and paste the <0>.onion address in the input below.<1/>Add your <0>port after the onion address, the default port is <0>:8080.", @@ -256,14 +246,12 @@ }, "bitcoin_beach": { "title": "Bitcoin Beach Wallet", - "description": "Create or connect to a Bitcoin Beach (Galoy) account", "page": { "title": "Connect to <0>Bitcoin Beach Wallet" } }, "bitcoin_jungle": { "title": "Bitcoin Jungle Wallet", - "description": "Create or connect to a Bitcoin Jungle (Galoy) account", "page": { "title": "Connect to <0>Bitcoin Jungle Wallet" } @@ -293,7 +281,6 @@ }, "btcpay": { "title": "BTCPay Server", - "description": "Connect to your BTCPay LND node", "page": { "title": "Connect to your BTCPay LND node", "instructions": "Navigate to your BTCPayServer and log in as an admin. Go to Server Settings > Services > LND Rest - See information. Then Click \"See QR Code information\" and copy the QR Code data. Paste it below:" @@ -308,7 +295,6 @@ }, "commando": { "title": "Core Lightning", - "description": "Connect to your Core Lightning node", "page": { "title": "Connect to your Core Lightning node", "instructions": "Make sure you have Core Lightning version 0.12.0 or newer, the commando plugin is running and your node is accessible over the Lightning Network. Create a rune by running 'lightning-cli commando-rune'." From 645b19dc8686adc48949d50e0a16581d31ab052c Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 15 Dec 2022 18:44:34 +0700 Subject: [PATCH 028/141] chore: update commonCreateWalletUserCreate argument to make usage clearer --- tests/e2e/001-createWallets.spec.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/e2e/001-createWallets.spec.ts b/tests/e2e/001-createWallets.spec.ts index b7d821ab89..1978419de6 100644 --- a/tests/e2e/001-createWallets.spec.ts +++ b/tests/e2e/001-createWallets.spec.ts @@ -9,7 +9,9 @@ const { getByText, getByLabelText, findByLabelText, findByText } = queries; const user = USER.SINGLE(); const commonCreateWalletUserCreate = async ( - connectToLightningWallet = true + options: { connectToLightningWallet: boolean } = { + connectToLightningWallet: true, + } ) => { const { page, browser } = await loadExtension(); @@ -50,7 +52,7 @@ const commonCreateWalletUserCreate = async ( await findByText($document, "Create or Connect Wallet"); - if (connectToLightningWallet) { + if (options.connectToLightningWallet) { const chooseConnectorButton = await findByText( $document, "Connect to Lightning Wallet" @@ -84,7 +86,7 @@ const commonCreateWalletSuccessCheck = async ({ page, $document }) => { test.describe("Create or connect wallets", () => { test("successfully creates an Alby wallet", async () => { const { user, browser, page, $document } = - await commonCreateWalletUserCreate(false); + await commonCreateWalletUserCreate({ connectToLightningWallet: false }); // click at "Create Alby Wallet" const createNewWalletButton = await getByText($document, "Create new"); From 1e2dbffd73b4609c0ff6c81a0d0a1dc29e8bb1ed Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 15 Dec 2022 19:09:58 +0700 Subject: [PATCH 029/141] chore: add e2e test to connect to an existing alby wallet --- tests/e2e/001-createWallets.spec.ts | 44 ++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/tests/e2e/001-createWallets.spec.ts b/tests/e2e/001-createWallets.spec.ts index 1978419de6..146e3d5598 100644 --- a/tests/e2e/001-createWallets.spec.ts +++ b/tests/e2e/001-createWallets.spec.ts @@ -1,18 +1,25 @@ import { test } from "@playwright/test"; import { USER } from "complete-randomer"; import { getDocument, queries } from "pptr-testing-library"; +import { Browser, ElementHandle, Page } from "puppeteer"; import { loadExtension } from "./helpers/loadExtension"; const { getByText, getByLabelText, findByLabelText, findByText } = queries; -const user = USER.SINGLE(); +type User = { email: string; password: string }; +const defaultUser = USER.SINGLE() as User; const commonCreateWalletUserCreate = async ( - options: { connectToLightningWallet: boolean } = { + options: { connectToLightningWallet: boolean; user?: User } = { connectToLightningWallet: true, } -) => { +): Promise<{ + user: User; + page: Page; + browser: Browser; + $document: ElementHandle; +}> => { const { page, browser } = await loadExtension(); // get document from welcome page @@ -67,7 +74,7 @@ const commonCreateWalletUserCreate = async ( await findByText($document, "Connect Lightning Wallet"); } - return { user, browser, page, $document }; + return { user: options.user || defaultUser, browser, page, $document }; }; const commonCreateWalletSuccessCheck = async ({ page, $document }) => { @@ -107,6 +114,35 @@ test.describe("Create or connect wallets", () => { await browser.close(); }); + test("successfully connects to an existing Alby testnet wallet", async () => { + const { user, browser, page, $document } = + await commonCreateWalletUserCreate({ + connectToLightningWallet: false, + user: { + email: "albytest001@example.com", + password: "12345678", + }, + }); + + // click at "Create Alby Wallet" + const createNewWalletButton = await getByText($document, "Create new"); + createNewWalletButton.click(); + + await findByText($document, "Your Alby Lightning Wallet"); + + // type user email + const emailField = await getByLabelText($document, "Email Address"); + await emailField.type(user.email); + + // type user password and confirm password + const walletPasswordField = await getByLabelText($document, "Password"); + await walletPasswordField.type(user.password); + + await commonCreateWalletSuccessCheck({ page, $document }); + + await browser.close(); + }); + test("successfully connects to LNbits wallet", async () => { const { browser, page, $document } = await commonCreateWalletUserCreate(); From 96bce7f9e54902aacdcdae102ce1db2931bc1e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=B6gel?= Date: Thu, 15 Dec 2022 14:34:34 +0100 Subject: [PATCH 030/141] README: + move all dev setup instructions to one place. Before they were scattered and you could miss esssential steps when only looking at SETUP.md. SETUP: + add note on how to talk to production API (WALLET_CREATE_URL). --- README.md | 10 ---------- doc/SETUP.md | 13 +++++++++++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4d71d3a6b7..caa0fa00e0 100644 --- a/README.md +++ b/README.md @@ -81,16 +81,6 @@ Then run the following ### 🛠 Development -- Install dependencies\ - `yarn install` -- To watch file changes in development - - Chrome\ - `yarn run dev:chrome` - - Firefox\ - `yarn run dev:firefox` - - Opera\ - `yarn run dev:opera` - [Refer to SETUP.md for info regarding how to setup Alby](./doc/SETUP.md) ## Native Companions diff --git a/doc/SETUP.md b/doc/SETUP.md index aa5faae322..ffe9c21ddd 100644 --- a/doc/SETUP.md +++ b/doc/SETUP.md @@ -2,8 +2,21 @@ ## 🚀 Quick Start +- Install dependencies\ + `$ yarn install` + ### 💻 Load extension into browser +- Start development build, which will automatically watch for file changes: + + - Chrome\ + `$ yarn run dev:chrome` + - Firefox\ + `$ yarn run dev:firefox` + - Opera\ + `$ yarn run dev:opera` + **NOTE:** by default, the extension built this way will talk to the development regtest API. In case you want to do manual tests against the production API, add the following `WALLET_CREATE_URL` environment variable to your command: `$ WALLET_CREATE_URL="https://getalby.com/api/users" yarn run dev:your-browser-of-choice` + - **Chrome** - Go to the browser address bar and type `chrome://extensions` From 13d5a83399341ba187ba4bb5ba855c151bcaa8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=B6gel?= <100707419+jankoegel@users.noreply.github.com> Date: Thu, 15 Dec 2022 14:45:57 +0100 Subject: [PATCH 031/141] Update doc/SETUP.md Co-authored-by: Michael Bumann --- doc/SETUP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/SETUP.md b/doc/SETUP.md index ffe9c21ddd..13ef18014c 100644 --- a/doc/SETUP.md +++ b/doc/SETUP.md @@ -15,7 +15,7 @@ `$ yarn run dev:firefox` - Opera\ `$ yarn run dev:opera` - **NOTE:** by default, the extension built this way will talk to the development regtest API. In case you want to do manual tests against the production API, add the following `WALLET_CREATE_URL` environment variable to your command: `$ WALLET_CREATE_URL="https://getalby.com/api/users" yarn run dev:your-browser-of-choice` + **NOTE:** by default, the extension built this way will talk to the testnet API (which runs under [app.regtest.getalby.com](https://app.regtest.getalby.com/user)). In case you want to do manual tests against the mainnet API, add the following `WALLET_CREATE_URL` environment variable to your command: `$ WALLET_CREATE_URL="https://getalby.com/api/users" yarn run dev:your-browser-of-choice` - **Chrome** From 2094c98c6a73b108459d0f70a57fa56e1be5547f Mon Sep 17 00:00:00 2001 From: escapedcat Date: Thu, 15 Dec 2022 15:43:50 +0100 Subject: [PATCH 032/141] test(actions): authFunction success/fail #1874 --- .../actions/lnurl/__tests__/auth.test.ts | 54 +++++++++++++++++++ .../background-script/actions/lnurl/auth.ts | 8 +-- tests/fixtures/msw/handlers.ts | 18 +++++++ 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/extension/background-script/actions/lnurl/__tests__/auth.test.ts diff --git a/src/extension/background-script/actions/lnurl/__tests__/auth.test.ts b/src/extension/background-script/actions/lnurl/__tests__/auth.test.ts new file mode 100644 index 0000000000..3804e8a0c7 --- /dev/null +++ b/src/extension/background-script/actions/lnurl/__tests__/auth.test.ts @@ -0,0 +1,54 @@ +import { settingsFixture as mockSettings } from "~/../tests/fixtures/settings"; +import state from "~/extension/background-script/state"; +import type { LNURLDetails } from "~/types"; + +import { authFunction } from "../auth"; + +jest.mock("~/extension/background-script/state"); + +const mockState = { + settings: mockSettings, + getConnector: () => ({ + signMessage: () => + Promise.resolve({ + data: { + signature: + "rnu5pnhanjs3bfxz33fuyf9ywzrmkm1ns6jxdraxff1irq3hpxcbkce6zk34ee9bh7bamgd891tfy4gq1y119w53qg1ap5zodwi4u51n", + }, + }), + }), +}; + +const lnurlDetails: LNURLDetails = { + domain: "lnurl.fiatjaf.com", + k1: "dea6a5e410ae8db8872b30ed715d9c10bbaca1dda653396511a40bb353529572", + tag: "login", + url: "https://lnurl.fiatjaf.com/lnurl-login", +}; + +describe("auth", () => { + test("returns success response", async () => { + state.getState = jest.fn().mockReturnValue(mockState); + + expect(await authFunction({ lnurlDetails })).toStrictEqual({ + success: true, + status: "OK", + reason: undefined, + authResponseData: { status: "OK" }, + }); + }); + + test("fails gracefully if no status is set", async () => { + const lnurlDetails: LNURLDetails = { + domain: "lnurl.fiatjaf.com", + k1: "dea6a5e410ae8db8872b30ed715d9c10bbaca1dda653396511a40bb353529572", + tag: "login", + url: "https://lnurl.fiatjaf.com/lnurl-login-fail", + }; + state.getState = jest.fn().mockReturnValue(mockState); + + expect(() => authFunction({ lnurlDetails })).rejects.toThrowError( + "Auth: Something went wrong" + ); + }); +}); diff --git a/src/extension/background-script/actions/lnurl/auth.ts b/src/extension/background-script/actions/lnurl/auth.ts index 407c22b890..7ff768b370 100644 --- a/src/extension/background-script/actions/lnurl/auth.ts +++ b/src/extension/background-script/actions/lnurl/auth.ts @@ -7,11 +7,11 @@ import utils from "~/common/lib/utils"; import HashKeySigner from "~/common/utils/signer"; import state from "~/extension/background-script/state"; import { - MessageLnurlAuth, - LNURLDetails, + AuthResponseObject, LnurlAuthResponse, + LNURLDetails, + MessageLnurlAuth, OriginData, - AuthResponseObject, } from "~/types"; const LNURLAUTH_CANONICAL_PHRASE = @@ -44,6 +44,7 @@ export async function authFunction({ key_index: 0, }, }); + const lnSignature = signResponse.data.signature; // make sure we got a signature @@ -64,6 +65,7 @@ export async function authFunction({ } else { linkingKeyPriv = hmacSHA256(url.host, Hex.parse(hashingKey)).toString(Hex); } + // make sure we got a hashingKey and a linkingkey (just to be sure for whatever reason) if (!hashingKey || !linkingKeyPriv) { throw new Error("Invalid hashingKey/linkingKey"); diff --git a/tests/fixtures/msw/handlers.ts b/tests/fixtures/msw/handlers.ts index 2beaab3ac0..bd5763b5fd 100644 --- a/tests/fixtures/msw/handlers.ts +++ b/tests/fixtures/msw/handlers.ts @@ -39,4 +39,22 @@ export const handlers = [ ); } ), + + rest.get("https://lnurl.fiatjaf.com/lnurl-login", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + status: "OK", + }) + ); + }), + + rest.get("https://lnurl.fiatjaf.com/lnurl-login-fail", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + status: null, + }) + ); + }), ]; From a9bbd5fb726d71a27b34573ce927c2d2fb863719 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Dec 2022 16:48:19 +0100 Subject: [PATCH 033/141] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/ --- src/i18n/locales/cs/translation.json | 1 - src/i18n/locales/eo/translation.json | 1 - src/i18n/locales/es/translation.json | 12 ++---------- src/i18n/locales/fi/translation.json | 1 - src/i18n/locales/it/translation.json | 2 -- src/i18n/locales/pt_BR/translation.json | 11 +---------- src/i18n/locales/sv/translation.json | 2 -- src/i18n/locales/tl/translation.json | 3 +-- src/i18n/locales/zh_Hans/translation.json | 3 +-- 9 files changed, 5 insertions(+), 31 deletions(-) diff --git a/src/i18n/locales/cs/translation.json b/src/i18n/locales/cs/translation.json index 9ea11fe2b0..ba3fc667e4 100644 --- a/src/i18n/locales/cs/translation.json +++ b/src/i18n/locales/cs/translation.json @@ -634,7 +634,6 @@ "confirm_request_permission": { "title": "", "allow": "", - "enable_method": "", "always_allow": "" }, "nostr": { diff --git a/src/i18n/locales/eo/translation.json b/src/i18n/locales/eo/translation.json index 40c4647019..9eadb6de9d 100644 --- a/src/i18n/locales/eo/translation.json +++ b/src/i18n/locales/eo/translation.json @@ -634,7 +634,6 @@ "confirm_request_permission": { "title": "Aprobi peton", "allow": "", - "enable_method": "", "always_allow": "" }, "nostr": { diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 2a63ea1a8e..6b3732224d 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -111,18 +111,12 @@ } }, "lndhub": { - "title": "LNDHub (billetera Bluewallet)", "description": "Conéctese a su billetera móvil Blue Wallet", "page": { - "title": "Conéctese a LNDHub (BlueWallet)", "description": "En la billetera BlueWallet, elija la billetera que desea conectar, ábrala, haga clic en\"...\", haga clic en Exportar / Copia de seguridad para mostrar el código QR y escanearlo con su cámara web." }, - "uri": { - "label": "Exportar URI de LNDHub" - }, "errors": { - "invalid_uri": "URI de LNDHub no válido", - "connection_failed": "La conexión falló. ¿Es correcto el URI de LNDHub?" + "invalid_uri": "URI de LNDHub no válido" } }, "lnbits": { @@ -274,7 +268,6 @@ "allow": "Permitir que {{host}}:", "request1": "Solicitar aprobación de transacciones", "request2": "Solicitar facturas e información sobre rayos", - "connect": "Conectar", "block_and_ignore": "Bloquear e ignorar {{host}}", "title": "Conectar" }, @@ -384,8 +377,7 @@ "title": "Esperando para escanear" }, "input": { - "label": "Factura, Dirección Lightning o LNURL", - "placeholder": "Pegar dirección de factura, lnurl o lightning" + "label": "Factura, Dirección Lightning o LNURL" } }, "lnurlpay": { diff --git a/src/i18n/locales/fi/translation.json b/src/i18n/locales/fi/translation.json index 087d0d7da3..1d0164f15d 100644 --- a/src/i18n/locales/fi/translation.json +++ b/src/i18n/locales/fi/translation.json @@ -634,7 +634,6 @@ "confirm_request_permission": { "title": "Hyväksy pyyntö", "allow": "Salli {{host}}:", - "enable_method": "{{metodi}}:n kutsu", "always_allow": "Salli aina {{metodi}} {{host}}:lla" }, "nostr": { diff --git a/src/i18n/locales/it/translation.json b/src/i18n/locales/it/translation.json index 25af26714b..7c70d1c7c0 100644 --- a/src/i18n/locales/it/translation.json +++ b/src/i18n/locales/it/translation.json @@ -611,7 +611,6 @@ "error": "Errore", "settings": "Impostazioni", "websites": "Siti web", - "sats": "sats", "loading": "caricamento", "amount": "Importo", "optional": "Facoltativo", @@ -619,7 +618,6 @@ "copied": "Copiato!", "description": "Descrizione", "description_full": "Descrizione completa", - "were_sent_to": "sono stati inviati a", "message": "Messaggio", "help": "Aiuto", "actions": { diff --git a/src/i18n/locales/pt_BR/translation.json b/src/i18n/locales/pt_BR/translation.json index aeea228959..c79eff0758 100644 --- a/src/i18n/locales/pt_BR/translation.json +++ b/src/i18n/locales/pt_BR/translation.json @@ -111,18 +111,12 @@ } }, "lndhub": { - "title": "LNDHub (Bluewallet)", "description": "Conectar carteira Bluewallet", "page": { - "title": "Conecte na LNDHub (BlueWallet)", "description": "Na BlueWallet, abra a carteira que você deseja conectar, clique em \"...\", clique em Exportar/Backup para exibir o código QR. Leia o código QR com sua webcam." }, - "uri": { - "label": "Exportar URI LNDHub" - }, "errors": { - "invalid_uri": "URI LNDHub inválida", - "connection_failed": "Falha na conexão. Tem certeza de que a URI da LNDHub está correta?" + "invalid_uri": "URI LNDHub inválida" } }, "lnbits": { @@ -390,7 +384,6 @@ "allow": "Permitir {{host}}:", "request1": "Solicitar aprovação para transações", "request2": "Gerar faturas e solicitar informações do servidor", - "connect": "Conectar", "block_and_ignore": "Bloquear e ignorar {{host}}", "title": "Conectar", "block_added": "{{host}} adicionado na lista de bloqueio, por favor recarregue o site." @@ -516,7 +509,6 @@ }, "input": { "label": "Destinatário", - "placeholder": "Cole uma fatura, lnurl ou um endereço relâmpago", "hint": "Fatura, Endereço Relâmpago ou LNURL" } }, @@ -668,7 +660,6 @@ "confirm_request_permission": { "title": "Aprovar Solicitação", "allow": "Permitir {{host}}:", - "enable_method": "Chamar {{method}}", "always_allow": "Sempre permitir {{method}} em {{host}}" } }, diff --git a/src/i18n/locales/sv/translation.json b/src/i18n/locales/sv/translation.json index 0e8042c3ef..17c017085e 100644 --- a/src/i18n/locales/sv/translation.json +++ b/src/i18n/locales/sv/translation.json @@ -611,7 +611,6 @@ "error": "Misslyckades", "settings": "Inställningar", "websites": "Webbplatser", - "sats": "sats", "loading": "laddar", "amount": "Belopp", "optional": "Valfritt", @@ -619,7 +618,6 @@ "copied": "Kopierad!", "description": "Beskrivning", "description_full": "Fullständig beskrivning", - "were_sent_to": "skickades till", "message": "Meddelande", "help": "Hjälp", "actions": { diff --git a/src/i18n/locales/tl/translation.json b/src/i18n/locales/tl/translation.json index 8329583fa8..20c694053e 100644 --- a/src/i18n/locales/tl/translation.json +++ b/src/i18n/locales/tl/translation.json @@ -455,8 +455,7 @@ "title": "" }, "input": { - "label": "", - "placeholder": "" + "label": "" } }, "lnurlpay": { diff --git a/src/i18n/locales/zh_Hans/translation.json b/src/i18n/locales/zh_Hans/translation.json index ab237898bb..51fd41ae57 100644 --- a/src/i18n/locales/zh_Hans/translation.json +++ b/src/i18n/locales/zh_Hans/translation.json @@ -595,8 +595,7 @@ "confirm_request_permission": { "allow": "允许{{host}}进行:", "always_allow": "始终允许在{{host}}上使用{{method}}", - "title": "批准请求", - "enable_method": "调用{{method}}" + "title": "批准请求" }, "nostr": { "title": "Nostr", From 46cd60bf0b4d15e3bf38d12cc973281ea854533f Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Dec 2022 16:54:30 +0100 Subject: [PATCH 034/141] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/ --- src/i18n/locales/es/translation.json | 4 ---- src/i18n/locales/pt_BR/translation.json | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 6b3732224d..c10a750582 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -111,12 +111,8 @@ } }, "lndhub": { - "description": "Conéctese a su billetera móvil Blue Wallet", "page": { "description": "En la billetera BlueWallet, elija la billetera que desea conectar, ábrala, haga clic en\"...\", haga clic en Exportar / Copia de seguridad para mostrar el código QR y escanearlo con su cámara web." - }, - "errors": { - "invalid_uri": "URI de LNDHub no válido" } }, "lnbits": { diff --git a/src/i18n/locales/pt_BR/translation.json b/src/i18n/locales/pt_BR/translation.json index c79eff0758..5fd684a7f8 100644 --- a/src/i18n/locales/pt_BR/translation.json +++ b/src/i18n/locales/pt_BR/translation.json @@ -111,12 +111,8 @@ } }, "lndhub": { - "description": "Conectar carteira Bluewallet", "page": { "description": "Na BlueWallet, abra a carteira que você deseja conectar, clique em \"...\", clique em Exportar/Backup para exibir o código QR. Leia o código QR com sua webcam." - }, - "errors": { - "invalid_uri": "URI LNDHub inválida" } }, "lnbits": { From 0b0f7a77237971fe1a9129825d230e80ad92da07 Mon Sep 17 00:00:00 2001 From: sgmoore Date: Thu, 15 Dec 2022 19:27:04 -0800 Subject: [PATCH 035/141] Minor grammar fixes Minor grammar fixes at lines 61, 98, and 134. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4d71d3a6b7..88778cc24d 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Add Alby to your browser ### Try out the most recent version of Alby (Nightly Releases) - [Firefox Nightly](https://nightly.link/getAlby/lightning-browser-extension/workflows/build/master/firefox.xpi.zip) - best to install it as a temporary add-on as discussed in the "Load extension into browser" section -- [Chrome Nightly](https://nightly.link/getAlby/lightning-browser-extension/workflows/build/master/chrome.zip) - go to `chrome://extensions/`, enable "Developer mode" (top right) and drag & drop the file in the browser +- [Chrome Nightly](https://nightly.link/getAlby/lightning-browser-extension/workflows/build/master/chrome.zip) - go to `chrome://extensions/`, enable "Developer mode" (top right), and drag & drop the file in the browser (Note: You might need to reconfigure your wallet after installing new versions) @@ -95,7 +95,7 @@ Then run the following ## Native Companions -Alby supports native connectors to native applications on the host computer. For this the extension passes each call to a native application (using [native messaging](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging)). +Alby supports native connectors to native applications on the host computer. For this, the extension passes each call to a native application (using [native messaging](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging)). This allows Alby also to connect to nodes behind Tor (through this native "proxy" application). Currently, there is one native companion app available to connect to Tor nodes: [https://github.com/getAlby/alby-companion-rs](https://github.com/getAlby/alby-companion-rs) @@ -132,7 +132,7 @@ We use the [Development Project Board](https://github.com/orgs/getAlby/projects/ Joule is a full interface to manage a LND node. It only supports one LND account. Our goal is NOT to write a full UI for a Lightning Network node with all the channel management features, but instead to only focus on what is necessary for the web (for payment and authentication flows). We believe there are already way better management UIs. -Also we focus on supporting multiple different node backends (non-custodial and custodial). +Also, we focus on supporting multiple different node backends (non-custodial and custodial). #### What is WebLN? From f3ef78edc9cc29d5e2ad0e513e059fcc664c423c Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 16 Dec 2022 11:00:55 +0100 Subject: [PATCH 036/141] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/ --- src/i18n/locales/es/translation.json | 5 ----- src/i18n/locales/pt_BR/translation.json | 5 ----- 2 files changed, 10 deletions(-) diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index c10a750582..b19706ae5f 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -110,11 +110,6 @@ "connection_failed": "La conexión falló. ¿Son correctas sus credenciales de LND?" } }, - "lndhub": { - "page": { - "description": "En la billetera BlueWallet, elija la billetera que desea conectar, ábrala, haga clic en\"...\", haga clic en Exportar / Copia de seguridad para mostrar el código QR y escanearlo con su cámara web." - } - }, "lnbits": { "title": "Cuenta LNbits", "description": "Conéctese a su cuenta de LNbits", diff --git a/src/i18n/locales/pt_BR/translation.json b/src/i18n/locales/pt_BR/translation.json index 5fd684a7f8..f10575a685 100644 --- a/src/i18n/locales/pt_BR/translation.json +++ b/src/i18n/locales/pt_BR/translation.json @@ -110,11 +110,6 @@ "connection_failed": "Falha na conexão. As credenciais LND estão corretas?" } }, - "lndhub": { - "page": { - "description": "Na BlueWallet, abra a carteira que você deseja conectar, clique em \"...\", clique em Exportar/Backup para exibir o código QR. Leia o código QR com sua webcam." - } - }, "lnbits": { "title": "LNbits", "description": "Conectar em uma conta LNbits", From bfc24ef7c7fd91cada0c90bc7b7db2541ef43578 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Fri, 16 Dec 2022 21:55:29 +0100 Subject: [PATCH 037/141] feat: always focus prompt This makes sure that the prompt is focussed every 2 seconds. If the user "looses" the prompt this change brings the prompt in focus again. --- src/common/lib/utils.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/common/lib/utils.ts b/src/common/lib/utils.ts index 93429da4a4..444bb508ee 100644 --- a/src/common/lib/utils.ts +++ b/src/common/lib/utils.ts @@ -80,6 +80,17 @@ const utils = { tabId = window.tabs[0].id; } + // make sure that the prompt is always focussed + // this interval focusses the prompt every 2 secons to make sure it is not hidden and + // that it draws the user's attention to the popup + const focusInterval = setInterval(() => { + if (!window.id) { + return; + } // mainly for TS I guess + browser.windows.update(window.id, { + focused: true, + }); + }, 2100); const onMessageListener = ( responseMessage: { response?: unknown; @@ -94,6 +105,7 @@ const utils = { sender.tab && sender.tab.id === tabId ) { + clearInterval(focusInterval); browser.tabs.onRemoved.removeListener(onRemovedListener); if (sender.tab.windowId) { return browser.windows.remove(sender.tab.windowId).then(() => { @@ -110,6 +122,7 @@ const utils = { }; const onRemovedListener = (tid: number) => { + clearInterval(focusInterval); if (tabId === tid) { browser.runtime.onMessage.removeListener(onMessageListener); reject(new Error(ABORT_PROMPT_ERROR)); From 68c32964371db153c94e33e24da5ee85e154eb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Sat, 17 Dec 2022 11:19:29 +0100 Subject: [PATCH 038/141] fix: window positioning --- src/common/lib/utils.ts | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/common/lib/utils.ts b/src/common/lib/utils.ts index 444bb508ee..6cbd891365 100644 --- a/src/common/lib/utils.ts +++ b/src/common/lib/utils.ts @@ -45,7 +45,7 @@ const utils = { openUrl: (url: string) => { browser.tabs.create({ url }); }, - openPrompt: (message: { + openPrompt: async (message: { args: Record; origin: OriginData | OriginDataInternal; action: string; @@ -66,13 +66,41 @@ const utils = { "prompt.html" )}?${urlParams.toString()}`; - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { + async function getPosition(w, h) { + let left = 0; + let top = 0; + try { + const lastFocused = await browser.windows.getLastFocused(); + // Position window in top right corner of lastFocused window. + top = lastFocused.top; + left = lastFocused.left + (lastFocused.width - w); + // Centered + // top = lastFocused.top + (lastFocused.height - h) / 2; + // left = lastFocused.left + (lastFocused.width - w) / 2; + } catch (_) { + // The following properties are more than likely 0, due to being + // opened from the background chrome process for the extension that + // has no physical dimensions + const { screenX, screenY, outerWidth } = window; + top = Math.max(screenY, 0); + left = Math.max(screenX + (outerWidth - w), 0); + } + return { + top, + left, + }; + } + + const { top, left } = await getPosition(400, 600); browser.windows .create({ url: url, type: "popup", width: 400, height: 600, + top: top, + left: left, }) .then((window) => { let tabId: number | undefined; @@ -90,7 +118,7 @@ const utils = { browser.windows.update(window.id, { focused: true, }); - }, 2100); + }, 1); const onMessageListener = ( responseMessage: { response?: unknown; From b9cccd85eed1017c46796f4613a76ef3039fc6cd Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Sat, 17 Dec 2022 13:11:01 +0000 Subject: [PATCH 039/141] Update all development Yarn dependencies (2022-12-17) --- package.json | 12 +++---- yarn.lock | 88 ++++++++++++++++++++++++++-------------------------- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 572cfb850e..ef0e62e713 100644 --- a/package.json +++ b/package.json @@ -73,9 +73,9 @@ "@commitlint/cli": "^17.3.0", "@commitlint/config-conventional": "^17.3.0", "@jest/types": "^29.3.1", - "@playwright/test": "^1.28.0", + "@playwright/test": "^1.28.1", "@swc/core": "^1.3.21", - "@swc/jest": "^0.2.23", + "@swc/jest": "^0.2.24", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", @@ -114,7 +114,7 @@ "jest-environment-jsdom": "^29.3.1", "jest-webextension-mock": "^3.8.6", "lint-staged": "^13.0.4", - "mini-css-extract-plugin": "^2.7.0", + "mini-css-extract-plugin": "^2.7.2", "msw": "^0.49.0", "postcss": "^8.4.19", "postcss-cli": "^10.1.0", @@ -123,16 +123,16 @@ "prettier": "^2.8.0", "process": "^0.11.10", "puppeteer": "^19.3.0", - "sass": "^1.56.1", + "sass": "^1.56.2", "sass-loader": "^13.2.0", "stream-browserify": "^3.0.0", "swc-loader": "^0.2.3", "terser-webpack-plugin": "^5.3.6", "tsconfig-paths-webpack-plugin": "^4.0.0", - "typescript": "^4.9.3", + "typescript": "^4.9.4", "webpack": "^5.75.0", "webpack-bundle-analyzer": "^4.7.0", - "webpack-cli": "^5.0.0", + "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.11.1", "webpack-sources": "^3.2.3", "wext-manifest-loader": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 3c0db3655e..7709d93365 100644 --- a/yarn.lock +++ b/yarn.lock @@ -984,13 +984,13 @@ resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== -"@playwright/test@^1.28.0": - version "1.28.0" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.28.0.tgz#8de83f9d2291bba3f37883e33431b325661720d9" - integrity sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ== +"@playwright/test@^1.28.1": + version "1.28.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.28.1.tgz#e5be297e024a3256610cac2baaa9347fd57c7860" + integrity sha512-xN6spdqrNlwSn9KabIhqfZR7IWjPpFK1835tFNgjrlysaSezuX8PYUwaz38V/yI8TJLG9PkAMEXoHRXYXlpTPQ== dependencies: "@types/node" "*" - playwright-core "1.28.0" + playwright-core "1.28.1" "@polka/url@^1.0.0-next.20": version "1.0.0-next.21" @@ -1087,10 +1087,10 @@ "@swc/core-win32-ia32-msvc" "1.3.21" "@swc/core-win32-x64-msvc" "1.3.21" -"@swc/jest@^0.2.23": - version "0.2.23" - resolved "https://registry.yarnpkg.com/@swc/jest/-/jest-0.2.23.tgz#0b7499d5927faaa090c5b7a4a0e35122968fef30" - integrity sha512-ZLj17XjHbPtNsgqjm83qizENw05emLkKGu3WuPUttcy9hkngl0/kcc7fDbcSBpADS0GUtsO+iKPjZFWVAtJSlA== +"@swc/jest@^0.2.24": + version "0.2.24" + resolved "https://registry.yarnpkg.com/@swc/jest/-/jest-0.2.24.tgz#35d9377ede049613cd5fdd6c24af2b8dcf622875" + integrity sha512-fwgxQbM1wXzyKzl1+IW0aGrRvAA8k0Y3NxFhKigbPjOJ4mCKnWEcNX9HQS3gshflcxq8YKhadabGUVfdwjCr6Q== dependencies: "@jest/create-cache-key-function" "^27.4.2" jsonc-parser "^3.2.0" @@ -1907,20 +1907,20 @@ "@webassemblyjs/ast" "1.11.1" "@xtuc/long" "4.2.2" -"@webpack-cli/configtest@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.0.0.tgz#5e1bc37064c7d00e1330641fa523f8ff85a39513" - integrity sha512-war4OU8NGjBqU3DP3bx6ciODXIh7dSXcpQq+P4K2Tqyd8L5OjZ7COx9QXx/QdCIwL2qoX09Wr4Cwf7uS4qdEng== +"@webpack-cli/configtest@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.0.1.tgz#a69720f6c9bad6aef54a8fa6ba9c3533e7ef4c7f" + integrity sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A== -"@webpack-cli/info@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.0.tgz#5a58476b129ee9b462117b23393596e726bf3b80" - integrity sha512-NNxDgbo4VOkNhOlTgY0Elhz3vKpOJq4/PKeKg7r8cmYM+GQA9vDofLYyup8jS6EpUvhNmR30cHTCEIyvXpskwA== +"@webpack-cli/info@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.1.tgz#eed745799c910d20081e06e5177c2b2569f166c0" + integrity sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA== -"@webpack-cli/serve@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.0.tgz#f08ea194e01ed45379383a8886e8c85a65a5f26a" - integrity sha512-Rumq5mHvGXamnOh3O8yLk1sjx8dB30qF1OeR6VC00DIR6SLJ4bwwUGKC4pE7qBFoQyyh0H9sAg3fikYgAqVR0w== +"@webpack-cli/serve@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.1.tgz#34bdc31727a1889198855913db2f270ace6d7bf8" + integrity sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw== "@xmldom/xmldom@^0.7.5": version "0.7.5" @@ -6623,10 +6623,10 @@ min-indent@^1.0.0, min-indent@^1.0.1: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -mini-css-extract-plugin@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.0.tgz#d7d9ba0c5b596d155e36e2b174082fc7f010dd64" - integrity sha512-auqtVo8KhTScMsba7MbijqZTfibbXiBNlPAQbsVt7enQfcDYLdgG57eGxMqwVU3mfeWANY4F1wUg+rMF+ycZgw== +mini-css-extract-plugin@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.2.tgz#e049d3ea7d3e4e773aad585c6cb329ce0c7b72d7" + integrity sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw== dependencies: schema-utils "^4.0.0" @@ -7293,10 +7293,10 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.28.0: - version "1.28.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.28.0.tgz#61df5c714f45139cca07095eccb4891e520e06f2" - integrity sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA== +playwright-core@1.28.1: + version "1.28.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.28.1.tgz#8400be9f4a8d1c0489abdb9e75a4cc0ffc3c00cb" + integrity sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag== postcss-calc@^8.2.3: version "8.2.4" @@ -8293,10 +8293,10 @@ sass-loader@^13.2.0: klona "^2.0.4" neo-async "^2.6.2" -sass@^1.56.1: - version "1.56.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.1.tgz#94d3910cd468fd075fa87f5bb17437a0b617d8a7" - integrity sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ== +sass@^1.56.2: + version "1.56.2" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.2.tgz#9433b345ab3872996c82a53a58c014fd244fd095" + integrity sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -9253,10 +9253,10 @@ typescript@^4.6.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== -typescript@^4.9.3: - version "4.9.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" - integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== +typescript@^4.9.4: + version "4.9.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" + integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== typeson-registry@^1.0.0-alpha.20: version "1.0.0-alpha.39" @@ -9518,15 +9518,15 @@ webpack-bundle-analyzer@^4.7.0: sirv "^1.0.7" ws "^7.3.1" -webpack-cli@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.0.0.tgz#bd380a9653e0cd1a08916c4ff1adea17201ef68f" - integrity sha512-AACDTo20yG+xn6HPW5xjbn2Be4KUzQPebWXsDMHwPPyKh9OnTOJgZN2Nc+g/FZKV3ObRTYsGvibAvc+5jAUrVA== +webpack-cli@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.0.1.tgz#95fc0495ac4065e9423a722dec9175560b6f2d9a" + integrity sha512-S3KVAyfwUqr0Mo/ur3NzIp6jnerNpo7GUO6so51mxLi1spqsA17YcMXy0WOIJtBSnj748lthxC6XLbNKh/ZC+A== dependencies: "@discoveryjs/json-ext" "^0.5.0" - "@webpack-cli/configtest" "^2.0.0" - "@webpack-cli/info" "^2.0.0" - "@webpack-cli/serve" "^2.0.0" + "@webpack-cli/configtest" "^2.0.1" + "@webpack-cli/info" "^2.0.1" + "@webpack-cli/serve" "^2.0.1" colorette "^2.0.14" commander "^9.4.1" cross-spawn "^7.0.3" From 8945f03ea217b20f7fb47854a426bc7e14cd0320 Mon Sep 17 00:00:00 2001 From: zhangchao <544262408@qq.com> Date: Mon, 19 Dec 2022 10:31:43 +0800 Subject: [PATCH 040/141] fix: allowances enable unit test --- .../actions/allowances/__tests__/enable.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/background-script/actions/allowances/__tests__/enable.test.ts b/src/extension/background-script/actions/allowances/__tests__/enable.test.ts index 6892081a94..ba96947c36 100644 --- a/src/extension/background-script/actions/allowances/__tests__/enable.test.ts +++ b/src/extension/background-script/actions/allowances/__tests__/enable.test.ts @@ -22,7 +22,7 @@ utils.openPrompt = jest .fn() .mockReturnValue({ data: { enabled: true, remember: true } }); -const mockAllowances: DbAllowance[] = allowanceFixture; +const mockAllowances: DbAllowance[] = [{ ...allowanceFixture[0] }]; describe("enable allowance", () => { afterEach(() => { From 2bd9e81ea7c956147d1208857846a4bf5670caf0 Mon Sep 17 00:00:00 2001 From: Lisa Oppermann Date: Thu, 15 Dec 2022 18:41:20 +0100 Subject: [PATCH 041/141] test: ln request --- .../actions/ln/__tests__/request.test.ts | 298 ++++++++++++++++++ .../background-script/actions/ln/request.ts | 33 +- src/extension/background-script/state.ts | 2 +- 3 files changed, 322 insertions(+), 11 deletions(-) create mode 100644 src/extension/background-script/actions/ln/__tests__/request.test.ts diff --git a/src/extension/background-script/actions/ln/__tests__/request.test.ts b/src/extension/background-script/actions/ln/__tests__/request.test.ts new file mode 100644 index 0000000000..1fde30e8f6 --- /dev/null +++ b/src/extension/background-script/actions/ln/__tests__/request.test.ts @@ -0,0 +1,298 @@ +import utils from "~/common/lib/utils"; +import type Connector from "~/extension/background-script/connectors/connector.interface"; +import db from "~/extension/background-script/db"; +import state, { State } from "~/extension/background-script/state"; +import type { MessageGenericRequest, OriginData } from "~/types"; + +import request from "../request"; + +jest.mock("~/extension/background-script/state"); +jest.mock("~/common/lib/utils", () => ({ + openPrompt: jest.fn(() => Promise.resolve({ data: {} })), +})); + +// suppress console logs when running tests +console.error = jest.fn(); + +const allowanceInDB = { + enabled: true, + host: "pro.kollider.xyz", + id: 1, + imageURL: "https://pro.kollider.xyz/favicon.ico", + lastPaymentAt: 0, + lnurlAuth: true, + name: "pro kollider", + remainingBudget: 500, + totalBudget: 500, + createdAt: "123456", + tag: "", +}; + +const permissionInDB = { + id: 1, + allowanceId: allowanceInDB.id, + createdAt: "1487076708000", + host: allowanceInDB.host, + method: "webln/makeinvoice", + blocked: false, + enabled: true, +}; + +const message: MessageGenericRequest = { + action: "request", + origin: { host: allowanceInDB.host } as OriginData, + args: { + method: "makeInvoice", + params: {}, + }, +}; + +const requestResponse = { data: [] }; +const fullConnector = { + requestMethod: jest.fn(() => Promise.resolve(requestResponse)), + supportedMethods: [ + // saved and compared in lowercase + "getinfo", + "makeinvoice", + "sendpayment", + ], +} as unknown as Connector; + +// overwrite "connector" in test +let connector: Connector; +const ConnectorClass = jest.fn().mockImplementation(() => { + return connector; +}); + +// prepare state +state.getState = () => + ({ + getConnector: jest.fn(() => Promise.resolve(new ConnectorClass())), + } as unknown as State); + +// prepare DB with allowance +db.allowances.bulkAdd([allowanceInDB]); + +// reset after every test +afterEach(async () => { + jest.clearAllMocks(); + // ensure a clear permission table in DB + await db.permissions.clear(); + // set a default connector if overwritten in a previous test + connector = fullConnector; +}); + +describe("ln request", () => { + describe("throws error", () => { + test("if connector does not support requestMethod", async () => { + connector = { + ...fullConnector, + supportedMethods: ["getinfo"], + }; + + const result = await request(message); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(result).toStrictEqual({ + error: "makeinvoice is not supported by your account", + }); + }); + + test("with unsupported method in message", async () => { + const messageWithUnsupportedMethod = { + ...message, + args: { + ...message.args, + method: "methodWithCamelCase", + }, + }; + + const result = await request(messageWithUnsupportedMethod); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(result).toStrictEqual({ + error: "methodwithcamelcase is not supported by your account", + }); + }); + + test("if the host's allowance does not exist", async () => { + const messageWithUndefinedAllowanceHost = { + ...message, + origin: { + ...message.origin, + host: "some-host", + }, + }; + + const result = await request(messageWithUndefinedAllowanceHost); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(result).toStrictEqual({ + error: "Could not find an allowance for this host", + }); + }); + + test("if the request itself throws", async () => { + connector = { + ...fullConnector, + requestMethod: jest.fn(() => + Promise.reject(new Error("Some API error")) + ), + }; + + const result = await request(message); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(result).toStrictEqual({ + error: "Some API error", + }); + }); + }); + + describe("directly calls requestMethod of Connector with method and params", () => { + test("if permission for this request exists and is enabled", async () => { + // prepare DB with matching permission + await db.permissions.bulkAdd([permissionInDB]); + + const result = await request(message); + + expect(connector.requestMethod).toHaveBeenCalledWith( + message.args.method.toLowerCase(), + message.args.params + ); + expect(result).toStrictEqual(requestResponse); + }); + }); + + describe("prompts the user first and then calls requestMethod", () => { + test("if the permission for this request does not exist", async () => { + // prepare DB with other permission + const otherPermission = { + ...permissionInDB, + method: "webln/sendpayment", + }; + await db.permissions.bulkAdd([otherPermission]); + + const result = await request(message); + + expect(utils.openPrompt).toHaveBeenCalledWith({ + args: { + requestPermission: { + method: message.args.method.toLowerCase(), + description: "object.makeinvoice", // jest doesn't give back the constructor's name? + }, + }, + origin: message.origin, + action: "public/confirmRequestPermission", + }); + expect(connector.requestMethod).toHaveBeenCalledWith( + message.args.method.toLowerCase(), + message.args.params + ); + expect(result).toStrictEqual(requestResponse); + }); + + test("if the permission for this request exists but is not enabled", async () => { + // prepare DB with disabled permission + const disabledPermission = { + ...permissionInDB, + enabled: false, + }; + await db.permissions.bulkAdd([disabledPermission]); + + const result = await request(message); + + expect(utils.openPrompt).toHaveBeenCalledWith({ + args: { + requestPermission: { + method: message.args.method.toLowerCase(), + description: "object.makeinvoice", // jest doesn't give back the constructor's name? + }, + }, + origin: message.origin, + action: "public/confirmRequestPermission", + }); + expect(connector.requestMethod).toHaveBeenCalledWith( + message.args.method.toLowerCase(), + message.args.params + ); + expect(result).toStrictEqual(requestResponse); + }); + }); + + describe("on the user's prompt response", () => { + test("saves the permission if enabled 'true'", async () => { + (utils.openPrompt as jest.Mock).mockResolvedValueOnce({ + data: { enabled: true, blocked: false }, + }); + // prepare DB with a permission + await db.permissions.bulkAdd([permissionInDB]); + + const messageWithOtherPermission = { + ...message, + args: { + ...message.args, + method: "getInfo", + }, + }; + + expect(await db.permissions.toArray()).toHaveLength(1); + expect( + await db.permissions.get({ method: "webln/getinfo" }) + ).toBeUndefined(); + + const result = await request(messageWithOtherPermission); + + expect(utils.openPrompt).toHaveBeenCalledTimes(1); + + expect(connector.requestMethod).toHaveBeenCalledWith( + messageWithOtherPermission.args.method.toLowerCase(), + messageWithOtherPermission.args.params + ); + + expect(await db.permissions.toArray()).toHaveLength(2); + expect( + await db.permissions.get({ method: "webln/getinfo" }) + ).toBeDefined(); + + expect(result).toStrictEqual(requestResponse); + }); + + test("does not save the permission if enabled 'false'", async () => { + (utils.openPrompt as jest.Mock).mockResolvedValueOnce({ + data: { enabled: false, blocked: false }, + }); + // prepare DB with a permission + await db.permissions.bulkAdd([permissionInDB]); + + const messageWithOtherPermission = { + ...message, + args: { + ...message.args, + method: "sendPayment", + }, + }; + + expect(await db.permissions.toArray()).toHaveLength(1); + expect( + await db.permissions.get({ method: "webln/sendpayment" }) + ).toBeUndefined(); + + const result = await request(messageWithOtherPermission); + + expect(utils.openPrompt).toHaveBeenCalledTimes(1); + + expect(connector.requestMethod).toHaveBeenCalledWith( + messageWithOtherPermission.args.method.toLowerCase(), + messageWithOtherPermission.args.params + ); + + expect(await db.permissions.toArray()).toHaveLength(1); + expect( + await db.permissions.get({ method: "webln/sendpayment" }) + ).toBeUndefined(); + + expect(result).toStrictEqual(requestResponse); + }); + }); +}); diff --git a/src/extension/background-script/actions/ln/request.ts b/src/extension/background-script/actions/ln/request.ts index cc03f7853c..8feac485b6 100644 --- a/src/extension/background-script/actions/ln/request.ts +++ b/src/extension/background-script/actions/ln/request.ts @@ -13,7 +13,7 @@ const request = async ( const { origin, args } = message; - const method = args.method.toLowerCase(); + const methodInLowerCase = args.method.toLowerCase(); try { // Check if the current connector support the call @@ -22,8 +22,11 @@ const request = async ( // // important: this must throw to exit and return an error const supportedMethods = connector.supportedMethods || []; // allow the connector to control which methods can be called - if (!connector.requestMethod || !supportedMethods.includes(method)) { - throw new Error(`${method} is not supported by your account`); + if ( + !connector.requestMethod || + !supportedMethods.includes(methodInLowerCase) + ) { + throw new Error(`${methodInLowerCase} is not supported by your account`); } const allowance = await db.allowances @@ -32,11 +35,11 @@ const request = async ( .first(); if (!allowance?.id) { - return { error: "Could not find an allowance for this host" }; + throw new Error("Could not find an allowance for this host"); } // prefix method with webln to prevent potential naming conflicts (e.g. with nostr calls that also use the permissions) - const weblnMethod = `${WEBLN_PREFIX}${method}`; + const weblnMethod = `${WEBLN_PREFIX}${methodInLowerCase}`; const permission = await db.permissions .where("host") @@ -45,8 +48,15 @@ const request = async ( .first(); // request method is allowed to be called - if (permission && permission.enabled && supportedMethods.includes(method)) { - const response = await connector.requestMethod(method, args.params); + if ( + permission && + permission.enabled && + supportedMethods.includes(methodInLowerCase) + ) { + const response = await connector.requestMethod( + methodInLowerCase, + args.params + ); return response; } else { const promptResponse = await utils.openPrompt<{ @@ -55,15 +65,18 @@ const request = async ( }>({ args: { requestPermission: { - method, - description: `${connector.constructor.name.toLowerCase()}.${method}`, + method: methodInLowerCase, + description: `${connector.constructor.name.toLowerCase()}.${methodInLowerCase}`, }, }, origin, action: "public/confirmRequestPermission", }); - const response = await connector.requestMethod(method, args.params); + const response = await connector.requestMethod( + methodInLowerCase, + args.params + ); // add permission to db only if user decided to always allow this request if (promptResponse.data.enabled) { diff --git a/src/extension/background-script/state.ts b/src/extension/background-script/state.ts index 3c1c93f482..915eed106b 100644 --- a/src/extension/background-script/state.ts +++ b/src/extension/background-script/state.ts @@ -12,7 +12,7 @@ import connectors from "./connectors"; import type Connector from "./connectors/connector.interface"; import Nostr from "./nostr"; -interface State { +export interface State { account: Account | null; accounts: Accounts; migrations: Migration[] | null; From 485eee01048bbe701c4fc625b5cdee8479513efd Mon Sep 17 00:00:00 2001 From: Lisa Oppermann Date: Thu, 15 Dec 2022 18:56:11 +0100 Subject: [PATCH 042/141] test: ensure permission is correctly stored --- .../actions/ln/__tests__/request.test.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/extension/background-script/actions/ln/__tests__/request.test.ts b/src/extension/background-script/actions/ln/__tests__/request.test.ts index 1fde30e8f6..60b1e25456 100644 --- a/src/extension/background-script/actions/ln/__tests__/request.test.ts +++ b/src/extension/background-script/actions/ln/__tests__/request.test.ts @@ -228,7 +228,7 @@ describe("ln request", () => { // prepare DB with a permission await db.permissions.bulkAdd([permissionInDB]); - const messageWithOtherPermission = { + const messageWithGetInfo = { ...message, args: { ...message.args, @@ -241,19 +241,29 @@ describe("ln request", () => { await db.permissions.get({ method: "webln/getinfo" }) ).toBeUndefined(); - const result = await request(messageWithOtherPermission); + const result = await request(messageWithGetInfo); expect(utils.openPrompt).toHaveBeenCalledTimes(1); expect(connector.requestMethod).toHaveBeenCalledWith( - messageWithOtherPermission.args.method.toLowerCase(), - messageWithOtherPermission.args.params + messageWithGetInfo.args.method.toLowerCase(), + messageWithGetInfo.args.params ); expect(await db.permissions.toArray()).toHaveLength(2); - expect( - await db.permissions.get({ method: "webln/getinfo" }) - ).toBeDefined(); + + const addedPermission = await db.permissions.get({ + method: "webln/getinfo", + }); + expect(addedPermission).toEqual( + expect.objectContaining({ + method: "webln/getinfo", + enabled: true, + allowanceId: allowanceInDB.id, + host: allowanceInDB.host, + blocked: false, + }) + ); expect(result).toStrictEqual(requestResponse); }); From a38777323ff11b61b45217c9e6ff30d38334c742 Mon Sep 17 00:00:00 2001 From: Lisa Oppermann Date: Thu, 15 Dec 2022 19:00:30 +0100 Subject: [PATCH 043/141] test: do not call open prompt --- .../background-script/actions/ln/__tests__/request.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/extension/background-script/actions/ln/__tests__/request.test.ts b/src/extension/background-script/actions/ln/__tests__/request.test.ts index 60b1e25456..a9f536a777 100644 --- a/src/extension/background-script/actions/ln/__tests__/request.test.ts +++ b/src/extension/background-script/actions/ln/__tests__/request.test.ts @@ -160,6 +160,9 @@ describe("ln request", () => { message.args.method.toLowerCase(), message.args.params ); + + expect(utils.openPrompt).not.toHaveBeenCalled(); + expect(result).toStrictEqual(requestResponse); }); }); From b4724d9d9c015bd06c1bf212d30f7d717da88b23 Mon Sep 17 00:00:00 2001 From: Lisa Oppermann Date: Thu, 15 Dec 2022 19:09:57 +0100 Subject: [PATCH 044/141] refactor: better state mocking --- .../actions/ln/__tests__/request.test.ts | 32 +++++++++---------- src/extension/background-script/state.ts | 2 +- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/extension/background-script/actions/ln/__tests__/request.test.ts b/src/extension/background-script/actions/ln/__tests__/request.test.ts index a9f536a777..9023d30158 100644 --- a/src/extension/background-script/actions/ln/__tests__/request.test.ts +++ b/src/extension/background-script/actions/ln/__tests__/request.test.ts @@ -1,18 +1,28 @@ import utils from "~/common/lib/utils"; import type Connector from "~/extension/background-script/connectors/connector.interface"; import db from "~/extension/background-script/db"; -import state, { State } from "~/extension/background-script/state"; import type { MessageGenericRequest, OriginData } from "~/types"; import request from "../request"; -jest.mock("~/extension/background-script/state"); +// suppress console logs when running tests +console.error = jest.fn(); + jest.mock("~/common/lib/utils", () => ({ openPrompt: jest.fn(() => Promise.resolve({ data: {} })), })); -// suppress console logs when running tests -console.error = jest.fn(); +// overwrite "connector" in tests later +let connector: Connector; +const ConnectorClass = jest.fn().mockImplementation(() => { + return connector; +}); + +jest.mock("~/extension/background-script/state", () => ({ + getState: () => ({ + getConnector: jest.fn(() => Promise.resolve(new ConnectorClass())), + }), +})); const allowanceInDB = { enabled: true, @@ -58,22 +68,10 @@ const fullConnector = { ], } as unknown as Connector; -// overwrite "connector" in test -let connector: Connector; -const ConnectorClass = jest.fn().mockImplementation(() => { - return connector; -}); - -// prepare state -state.getState = () => - ({ - getConnector: jest.fn(() => Promise.resolve(new ConnectorClass())), - } as unknown as State); - // prepare DB with allowance db.allowances.bulkAdd([allowanceInDB]); -// reset after every test +// resets after every test afterEach(async () => { jest.clearAllMocks(); // ensure a clear permission table in DB diff --git a/src/extension/background-script/state.ts b/src/extension/background-script/state.ts index 915eed106b..3c1c93f482 100644 --- a/src/extension/background-script/state.ts +++ b/src/extension/background-script/state.ts @@ -12,7 +12,7 @@ import connectors from "./connectors"; import type Connector from "./connectors/connector.interface"; import Nostr from "./nostr"; -export interface State { +interface State { account: Account | null; accounts: Accounts; migrations: Migration[] | null; From 39b987a324551f1b0e59e496b010d27e3a464ed7 Mon Sep 17 00:00:00 2001 From: Lisa Oppermann Date: Thu, 15 Dec 2022 19:35:50 +0100 Subject: [PATCH 045/141] fix: ensure toLowerCase can be called --- .../actions/ln/__tests__/request.test.ts | 17 +++++++++++++++++ .../background-script/actions/ln/request.ts | 9 +++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/extension/background-script/actions/ln/__tests__/request.test.ts b/src/extension/background-script/actions/ln/__tests__/request.test.ts index 9023d30158..ab065a9c6f 100644 --- a/src/extension/background-script/actions/ln/__tests__/request.test.ts +++ b/src/extension/background-script/actions/ln/__tests__/request.test.ts @@ -145,6 +145,23 @@ describe("ln request", () => { error: "Some API error", }); }); + + test("if the message args are not correct", async () => { + const messageWithoutMethod = { + ...message, + args: { + ...message.args, + method: undefined, + }, + } as unknown as MessageGenericRequest; + + const result = await request(messageWithoutMethod); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(result).toStrictEqual({ + error: "Request method is missing or not correct", + }); + }); }); describe("directly calls requestMethod of Connector with method and params", () => { diff --git a/src/extension/background-script/actions/ln/request.ts b/src/extension/background-script/actions/ln/request.ts index 8feac485b6..73ed55757d 100644 --- a/src/extension/background-script/actions/ln/request.ts +++ b/src/extension/background-script/actions/ln/request.ts @@ -13,9 +13,14 @@ const request = async ( const { origin, args } = message; - const methodInLowerCase = args.method.toLowerCase(); - try { + // // check first if method exists, otherwise toLowerCase() will fail with a TypeError + if (!args.method || typeof args.method !== "string") { + throw new Error("Request method is missing or not correct"); + } + + const methodInLowerCase = args.method.toLowerCase(); + // Check if the current connector support the call // connectors maybe do not support `requestMethod` at all // connectors also specify a whitelist of supported methods that can be called From 633406cfc6dea6caf8b306a88c62fb028f856a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Mon, 19 Dec 2022 15:31:01 +0100 Subject: [PATCH 046/141] fix: ts --- src/common/lib/utils.ts | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/common/lib/utils.ts b/src/common/lib/utils.ts index 6cbd891365..cfc10a7ab5 100644 --- a/src/common/lib/utils.ts +++ b/src/common/lib/utils.ts @@ -67,24 +67,37 @@ const utils = { )}?${urlParams.toString()}`; return new Promise(async (resolve, reject) => { - async function getPosition(w, h) { + async function getPosition( + width: number, + height: number + ): Promise<{ top: number; left: number }> { let left = 0; let top = 0; try { const lastFocused = await browser.windows.getLastFocused(); // Position window in top right corner of lastFocused window. - top = lastFocused.top; - left = lastFocused.left + (lastFocused.width - w); - // Centered - // top = lastFocused.top + (lastFocused.height - h) / 2; - // left = lastFocused.left + (lastFocused.width - w) / 2; + if ( + lastFocused && + lastFocused.top != undefined && + lastFocused.left != undefined && + lastFocused.width != undefined && + lastFocused.height != undefined + ) { + // Top right + top = lastFocused.top ?? 0; + left = lastFocused.left + (lastFocused.width - width); + + // Centered + top = lastFocused.top + (lastFocused.height - height) / 2; + left = lastFocused.left + (lastFocused.width - width) / 2; + } } catch (_) { // The following properties are more than likely 0, due to being // opened from the background chrome process for the extension that // has no physical dimensions const { screenX, screenY, outerWidth } = window; top = Math.max(screenY, 0); - left = Math.max(screenX + (outerWidth - w), 0); + left = Math.max(screenX + (outerWidth - width), 0); } return { top, @@ -114,11 +127,11 @@ const utils = { const focusInterval = setInterval(() => { if (!window.id) { return; - } // mainly for TS I guess + } browser.windows.update(window.id, { - focused: true, + drawAttention: true, }); - }, 1); + }, 2100); const onMessageListener = ( responseMessage: { response?: unknown; From e15732e77b21537ccf56fe527bf2710fbec4c901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=B6gel?= <100707419+jankoegel@users.noreply.github.com> Date: Mon, 19 Dec 2022 16:46:00 +0100 Subject: [PATCH 047/141] Log in: two words when it's a verb. (#1890) * Log in: two words when it's a verb. * spec. --- src/i18n/locales/en/translation.json | 10 +++++----- tests/e2e/002-walletFeatures.spec.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 4dd3896aad..55f71c2f17 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -66,10 +66,10 @@ "description": "You need to first connect to a lightning wallet so that you can interact with your favorite websites that accept bitcoin lightning payments!", "alby": { "title": "Alby Wallet", - "description": "Create or login to your Alby account", + "description": "Create or log in to your Alby account", "pre_connect": { "title": "Your Alby Lightning Wallet", - "login_account": "Create or login to your Alby account.", + "login_account": "Create or log in to your Alby account.", "host_wallet": "We host a Lightning wallet for you!", "email": { "label": "Email Address" @@ -86,7 +86,7 @@ "title": "numbers and letters, at least 3 characters" }, "errors": { - "create_wallet_error": "Failed to login or create a new account. If you need help, please contact support@getalby.com" + "create_wallet_error": "Failed to log in or create a new account. If you need help, please contact support@getalby.com" } } }, @@ -523,7 +523,7 @@ "lnurlauth": { "title": "Authentication", "content_message": { - "heading": "Do you want to login to" + "heading": "Do you want to log in to" }, "submit": "Login", "success": "Login successful on {{name}}", @@ -711,7 +711,7 @@ }, "enable_login": { "title": "Enable website login", - "subtitle": "Automatically login without confirmation when the website requests." + "subtitle": "Automatically log in without confirmation when the website requests." }, "edit_allowance": { "title": "Edit Allowance", diff --git a/tests/e2e/002-walletFeatures.spec.ts b/tests/e2e/002-walletFeatures.spec.ts index 9dad6f7ac6..7bc607c3ba 100644 --- a/tests/e2e/002-walletFeatures.spec.ts +++ b/tests/e2e/002-walletFeatures.spec.ts @@ -116,7 +116,7 @@ test.describe("Wallet features", () => { await findAllByText($optionsdocument, "e2etest.getalby.com"); await findByText( $optionsdocument, - "Do you want to login to e2etest.getalby.com?" + "Do you want to log in to e2etest.getalby.com?" ); await findByText($optionsdocument, "Login"); From ce57be12b1ee8b94e79ff0b577701f8fb40d7a18 Mon Sep 17 00:00:00 2001 From: Moritz Kaminski Date: Mon, 19 Dec 2022 16:46:37 +0100 Subject: [PATCH 048/141] Added Vida to "Website" screen (#1887) --- src/app/screens/Publishers/websites.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/screens/Publishers/websites.json b/src/app/screens/Publishers/websites.json index e5f4368b95..6b8d6ce12c 100644 --- a/src/app/screens/Publishers/websites.json +++ b/src/app/screens/Publishers/websites.json @@ -142,6 +142,12 @@ "logo": "https://cdn.getalby-assets.com/alby-extension-website-screen/sats4likes.svg", "url": "https://www.sats4likes.com/" }, + { + "title": "Vida", + "subtitle": "Earn sats anytime someone wants to connect and chatt", + "logo": "https://cdn.getalby-assets.com/alby-extension-website-screen/vida_thumbnail.svg", + "url": "https://vida.page/" + }, { "title": "LNCal.com", "subtitle": "Get booked and paid in Bitcoin", From bfea82396ebc0c356a104e36d24eaa888f100822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=B6gel?= <100707419+jankoegel@users.noreply.github.com> Date: Mon, 19 Dec 2022 16:57:15 +0100 Subject: [PATCH 049/141] Update SETUP.md --- doc/SETUP.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/SETUP.md b/doc/SETUP.md index 13ef18014c..74fc2c9fd5 100644 --- a/doc/SETUP.md +++ b/doc/SETUP.md @@ -15,7 +15,8 @@ `$ yarn run dev:firefox` - Opera\ `$ yarn run dev:opera` - **NOTE:** by default, the extension built this way will talk to the testnet API (which runs under [app.regtest.getalby.com](https://app.regtest.getalby.com/user)). In case you want to do manual tests against the mainnet API, add the following `WALLET_CREATE_URL` environment variable to your command: `$ WALLET_CREATE_URL="https://getalby.com/api/users" yarn run dev:your-browser-of-choice` + + **NOTE:** by default, the extension built this way will talk to the testnet API (which runs under [app.regtest.getalby.com](https://app.regtest.getalby.com/user)). In case you want to do manual tests against the mainnet API, add the following `WALLET_CREATE_URL` environment variable to your command: `$ WALLET_CREATE_URL="https://getalby.com/api/users" yarn run dev:your-browser-of-choice` - **Chrome** From ebfda51d226639319999b7b7709569ad25f47b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=B6gel?= Date: Mon, 19 Dec 2022 17:00:48 +0100 Subject: [PATCH 050/141] prettier --- doc/SETUP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/SETUP.md b/doc/SETUP.md index 74fc2c9fd5..03ba73be03 100644 --- a/doc/SETUP.md +++ b/doc/SETUP.md @@ -15,7 +15,7 @@ `$ yarn run dev:firefox` - Opera\ `$ yarn run dev:opera` - + **NOTE:** by default, the extension built this way will talk to the testnet API (which runs under [app.regtest.getalby.com](https://app.regtest.getalby.com/user)). In case you want to do manual tests against the mainnet API, add the following `WALLET_CREATE_URL` environment variable to your command: `$ WALLET_CREATE_URL="https://getalby.com/api/users" yarn run dev:your-browser-of-choice` - **Chrome** From 6dd35107332c52568f288038dee98fa59e3da77c Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 20 Dec 2022 11:26:48 +0700 Subject: [PATCH 051/141] feat: split alby login and sign up flows --- src/app/components/PasswordForm/index.tsx | 105 +++++++----- src/app/router/Options/Options.tsx | 8 +- src/app/router/Welcome/Welcome.tsx | 10 +- .../{NewWallet => AlbyWallet}/index.tsx | 154 +++++++++--------- .../connectors/ChooseConnectorPath/index.tsx | 5 +- src/i18n/locales/en/translation.json | 21 ++- 6 files changed, 170 insertions(+), 133 deletions(-) rename src/app/screens/connectors/{NewWallet => AlbyWallet}/index.tsx (61%) diff --git a/src/app/components/PasswordForm/index.tsx b/src/app/components/PasswordForm/index.tsx index 048df4b2d8..ef41452597 100644 --- a/src/app/components/PasswordForm/index.tsx +++ b/src/app/components/PasswordForm/index.tsx @@ -7,17 +7,18 @@ import type { KeyPrefix } from "i18next"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -export type Props = { +export type PasswordFormData = { + password: string; + passwordConfirmation: string; +}; + +export type Props = { i18nKeyPrefix: KeyPrefix<"translation">; children?: React.ReactNode; - formData: { - password: string; - passwordConfirmation: string; - }; - setFormData: (formData: { - password: string; - passwordConfirmation: string; - }) => void; + formData: T; + setFormData: (formData: T) => void; + minLength?: number; + confirm?: boolean; }; type errorMessage = @@ -31,11 +32,15 @@ const initialErrors: Record = { passwordConfirmationErrorMessage: "", }; -export default function PasswordForm({ +export default function PasswordForm< + T extends PasswordFormData = PasswordFormData +>({ formData, setFormData, i18nKeyPrefix, -}: Props) { + minLength, + confirm = true, +}: Props) { const [errors, setErrors] = useState(initialErrors); const [passwordView, setPasswordView] = useState(false); const [passwordConfirmationView, setPasswordConfirmationView] = @@ -71,9 +76,9 @@ export default function PasswordForm({ let passwordConfirmationErrorMessage: errorMessage = ""; if (!formData.password) passwordErrorMessage = "enter_password"; - if (!formData.passwordConfirmation) { + if (confirm && !formData.passwordConfirmation) { passwordConfirmationErrorMessage = "confirm_password"; - } else if (formData.password !== formData.passwordConfirmation) { + } else if (confirm && formData.password !== formData.passwordConfirmation) { passwordConfirmationErrorMessage = "mismatched_password"; } setErrors({ @@ -93,6 +98,14 @@ export default function PasswordForm({ required onChange={handleChange} tabIndex={0} + minLength={minLength} + pattern={minLength ? `.{${minLength},}` : undefined} + title={ + minLength + ? `at least ${minLength} characters` + : undefined /*TODO: i18n */ + } + onBlur={validate} endAdornment={
-
- - setPasswordConfirmationView(!passwordConfirmationView) - } - > - {passwordConfirmationView ? ( - - ) : ( - - )} - - } - /> - {errors.passwordConfirmationErrorMessage && ( -

- {t(`errors.${errors.passwordConfirmationErrorMessage}`)} -

- )} -
+ {confirm && ( +
+ + setPasswordConfirmationView(!passwordConfirmationView) + } + > + {passwordConfirmationView ? ( + + ) : ( + + )} + + } + /> + {errors.passwordConfirmationErrorMessage && ( +

+ {t(`errors.${errors.passwordConfirmationErrorMessage}`)} +

+ )} +
+ )} ); } diff --git a/src/app/router/Options/Options.tsx b/src/app/router/Options/Options.tsx index 58e7e05014..3735aaa9c8 100644 --- a/src/app/router/Options/Options.tsx +++ b/src/app/router/Options/Options.tsx @@ -20,9 +20,9 @@ import { ToastContainer } from "react-toastify"; import Providers from "~/app/context/Providers"; import RequireAuth from "~/app/router/RequireAuth"; import getConnectorRoutes from "~/app/router/connectorRoutes"; +import AlbyWallet from "~/app/screens/connectors/AlbyWallet"; import ChooseConnector from "~/app/screens/connectors/ChooseConnector"; import ChooseConnectorPath from "~/app/screens/connectors/ChooseConnectorPath"; -import NewWallet from "~/app/screens/connectors/NewWallet"; import i18n from "~/i18n/i18nConfig"; function Options() { @@ -74,7 +74,11 @@ function Options() { /> } /> - } /> + } + /> + } /> , + path: "create", + element: , + }, + { + path: "login", + element: , }, { path: "choose-connector", diff --git a/src/app/screens/connectors/NewWallet/index.tsx b/src/app/screens/connectors/AlbyWallet/index.tsx similarity index 61% rename from src/app/screens/connectors/NewWallet/index.tsx rename to src/app/screens/connectors/AlbyWallet/index.tsx index 71d260cec7..e43353ecf6 100644 --- a/src/app/screens/connectors/NewWallet/index.tsx +++ b/src/app/screens/connectors/AlbyWallet/index.tsx @@ -1,7 +1,3 @@ -import { - HiddenIcon, - VisibleIcon, -} from "@bitcoin-design/bitcoin-icons-react/outline"; import ConnectorForm from "@components/ConnectorForm"; import TextField from "@components/form/TextField"; import LoginFailedToast from "@components/toasts/LoginFailedToast"; @@ -11,6 +7,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; +import PasswordForm from "~/app/components/PasswordForm"; import msg from "~/common/lib/msg"; const walletCreateUrl = @@ -25,17 +22,25 @@ interface LNDHubCreateResponse { lnAddress: string; } -export default function NewWallet() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [passwordView, setPasswordView] = useState(false); - const [lnAddress, setLnAddress] = useState(""); +export type Props = { + variant: "login" | "create"; +}; + +const initialFormData = { + password: "", + passwordConfirmation: "", + email: "", + lnAddress: "", +}; + +export default function AlbyWallet({ variant }: Props) { const [loading, setLoading] = useState(false); const navigate = useNavigate(); const { t } = useTranslation("translation", { keyPrefix: "alby", }); const { t: tCommon } = useTranslation("common"); + const [formData, setFormData] = useState(initialFormData); function signup(event: React.FormEvent) { setLoading(true); @@ -49,9 +54,9 @@ export default function NewWallet() { const timestamp = Math.floor(Date.now() / 1000); const body = JSON.stringify({ - email, - password, - lightning_addresses_attributes: [{ address: lnAddress }], // address must be provided as array, in theory we support multiple addresses per account + email: formData.email, + password: formData.password, + lightning_addresses_attributes: [{ address: formData.lnAddress }], // address must be provided as array, in theory we support multiple addresses per account }); headers.append("X-TS", timestamp.toString()); const mac = hmacSHA256(body, HMAC_VERIFY_HEADER_KEY).toString(Base64); @@ -143,13 +148,27 @@ export default function NewWallet() { title={t("pre_connect.title")} submitLoading={loading} onSubmit={signup} - submitDisabled={loading || password === "" || email === ""} + submitDisabled={ + loading || + formData.password === "" || + formData.email === "" || + (variant === "create" && + formData.password !== formData.passwordConfirmation) + } >
- {t("pre_connect.login_account")} -
- {t("pre_connect.host_wallet")} + {t( + variant === "create" + ? "pre_connect.create_account" + : "pre_connect.login_account" + )} + {variant === "create" && ( + <> +
+ {t("pre_connect.host_wallet")} + + )}
@@ -160,73 +179,58 @@ export default function NewWallet() { type="email" required onChange={(e) => { - setEmail(e.target.value.trim()); + setFormData({ ...formData, email: e.target.value.trim() }); }} />
- { - setPassword(e.target.value.trim()); - }} - endAdornment={ - - } + confirm={variant === "create"} />
-
-

- {t("pre_connect.optional_lightning_note.part1")}{" "} - - {t("pre_connect.optional_lightning_note.part2")} - - {t("pre_connect.optional_lightning_note.part3")} ( - - {t("pre_connect.optional_lightning_note.part4")} - - ) -

-
- { - setLnAddress(e.target.value.trim().split("@")[0]); // in case somebody enters a full address we simple remove the domain - }} - /> + {variant === "create" && ( +
+

+ {t("pre_connect.optional_lightning_note.part1")}{" "} + + {t("pre_connect.optional_lightning_note.part2")} + + {t("pre_connect.optional_lightning_note.part3")} ( + + {t("pre_connect.optional_lightning_note.part4")} + + ) +

+
+ { + const lnAddress = e.target.value.trim().split("@")[0]; // in case somebody enters a full address we simple remove the domain + setFormData({ ...formData, lnAddress }); + }} + /> +
-
+ )} ); } diff --git a/src/app/screens/connectors/ChooseConnectorPath/index.tsx b/src/app/screens/connectors/ChooseConnectorPath/index.tsx index 84e83cfb0b..0fbe86ec02 100644 --- a/src/app/screens/connectors/ChooseConnectorPath/index.tsx +++ b/src/app/screens/connectors/ChooseConnectorPath/index.tsx @@ -44,11 +44,10 @@ export default function ChooseConnectorPath({ title, description }: Props) { } actions={ <> - +
{variant === "login" && ( - +

)} {variant === "create" && (
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 5daf8e49b2..8c8b5da18c 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -87,6 +87,7 @@ "mismatched_password": "Passwords don't match." } }, + "forgot_password": "Forgot password?", "title": "Your Alby account", "create_account": "Create a new Alby account to to send and receive bitcoin payments.", "login_account": "Log in to connect your existing Alby account.", From 76101e8bf63afdadb6fd85e5ced96842033d7c98 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Thu, 5 Jan 2023 06:56:16 +0000 Subject: [PATCH 132/141] Update axios to version 1.2.2 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a719ed8118..ec28e221b3 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@noble/secp256k1": "^1.7.0", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/line-clamp": "^0.4.2", - "axios": "^1.2.1", + "axios": "^1.2.2", "bech32": "^2.0.0", "bolt11": "^1.4.0", "crypto-js": "^4.1.1", diff --git a/yarn.lock b/yarn.lock index ec054b8a5f..927d4b2fd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2352,10 +2352,10 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -axios@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.1.tgz#44cf04a3c9f0c2252ebd85975361c026cb9f864a" - integrity sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A== +axios@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.2.tgz#72681724c6e6a43a9fea860fc558127dbe32f9f1" + integrity sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q== dependencies: follow-redirects "^1.15.0" form-data "^4.0.0" From c64bf36de14dbda64ca648f2763709decb0bfd9c Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 5 Jan 2023 21:08:48 +0700 Subject: [PATCH 133/141] fix: minor onboarding issues --- src/app/components/Button/index.tsx | 2 +- src/i18n/locales/en/translation.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/Button/index.tsx b/src/app/components/Button/index.tsx index 65cd967797..34b5900012 100644 --- a/src/app/components/Button/index.tsx +++ b/src/app/components/Button/index.tsx @@ -46,7 +46,7 @@ const Button = forwardRef( primary ? "bg-orange-bitcoin text-white border border-transparent" : outline - ? "bg-white text-orange-bitcoin border border-orange-bitcoin" + ? "bg-white text-orange-bitcoin border border-orange-bitcoin dark:bg-surface-02dp" : `bg-white text-gray-700 dark:bg-surface-02dp dark:text-neutral-200 dark:border-neutral-800`, primary && !disabled && "hover:bg-orange-bitcoin-700", !primary && diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 8c8b5da18c..12dadfad8d 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -89,7 +89,7 @@ }, "forgot_password": "Forgot password?", "title": "Your Alby account", - "create_account": "Create a new Alby account to to send and receive bitcoin payments.", + "create_account": "Create a new Alby account to send and receive bitcoin payments.", "login_account": "Log in to connect your existing Alby account.", "host_wallet": "We host a Lightning wallet for you!", "email": { From 48faa8b2cb0aeb15f9c05ca5ba20ca31940a4500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= <100827540+reneaaron@users.noreply.github.com> Date: Thu, 5 Jan 2023 16:11:07 +0100 Subject: [PATCH 134/141] Fix/UI cleanup (#1940) * fix: decrease rounding of logos to avoid logo interference * fix: move add account button out of card * fix: decrease contrast for dropdown outline --- src/app/components/PublishersTable/index.tsx | 2 +- src/app/screens/Accounts/index.tsx | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/app/components/PublishersTable/index.tsx b/src/app/components/PublishersTable/index.tsx index 539a694fb1..df72462eed 100644 --- a/src/app/components/PublishersTable/index.tsx +++ b/src/app/components/PublishersTable/index.tsx @@ -38,7 +38,7 @@ export default function PublishersTable({
{publisher.host} { diff --git a/src/app/screens/Accounts/index.tsx b/src/app/screens/Accounts/index.tsx index 197392975c..7bcc74fb05 100644 --- a/src/app/screens/Accounts/index.tsx +++ b/src/app/screens/Accounts/index.tsx @@ -111,12 +111,9 @@ function AccountsScreen() { return ( -

- {t("title")} -

- -
-
+
+

{t("title")}

+
- +
+
{Object.keys(accounts).map((accountId) => { @@ -148,7 +146,7 @@ function AccountsScreen() {
- + From 4bcbbdf114095f806062c251155921f67145e824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Thu, 5 Jan 2023 19:52:18 +0100 Subject: [PATCH 135/141] fix: display name instead of url as the publishercard title --- src/app/screens/Publisher.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/screens/Publisher.tsx b/src/app/screens/Publisher.tsx index fb64b65c66..8c53325b97 100644 --- a/src/app/screens/Publisher.tsx +++ b/src/app/screens/Publisher.tsx @@ -5,7 +5,7 @@ import PublisherCard from "@components/PublisherCard"; import TransactionsTable from "@components/TransactionsTable"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; -import { useState, useEffect, useRef, useCallback } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; @@ -75,7 +75,7 @@ function Publisher() {
Date: Thu, 5 Jan 2023 23:53:44 +0100 Subject: [PATCH 136/141] feat: add translation --- src/i18n/locales/en/translation.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 88833cd927..49fcdbbbe2 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -838,14 +838,15 @@ "getnetworkinfo": "Get basic stats about the known channel graph.", "getnodeinfo": "Get the channel information for a node.", "gettransactions": "Get a list of all transactions relevant to the wallet.", - "listpayments": "list of all outgoing payments.", - "listpeers": "list all currently active peers.", + "listpayments": "Get a list of all outgoing payments.", + "listpeers": "Get a list all currently active peers.", "lookupinvoice": "Look up invoice details.", "queryroutes": "Query for a possible route.", "verifymessage": "Verify a signature over a msg.", "sendtoroute": "Make a payment via the specified route.", "decodepayreq": "Decode a payment request string.", - "routermc": "Read the internal mission control state." + "routermc": "Read the internal mission control state.", + "addinvoice": "Create new invoices." } } } From 39cb996cf9f69ae08a4f1909ea39f7464eda2251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Fri, 6 Jan 2023 12:50:15 +0100 Subject: [PATCH 137/141] fix: improve handling for mobile browsers --- src/common/lib/utils.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/common/lib/utils.ts b/src/common/lib/utils.ts index 45563db633..6d1d0adc89 100644 --- a/src/common/lib/utils.ts +++ b/src/common/lib/utils.ts @@ -88,6 +88,12 @@ const utils = { tabId = window.tabs[0].id; } + // Kiwi Browser opens the prompt in the same window (there are only tabs on mobile browsers) + // Find the currently active tab to validate messages + if (window.tabs && window.tabs?.length > 1) { + tabId = window.tabs?.find((x) => x.active)?.id; + } + // this interval hightlights the popup in the taskbar const focusInterval = setInterval(() => { if (!window.id) { @@ -97,6 +103,7 @@ const utils = { drawAttention: true, }); }, 2100); + const onMessageListener = ( responseMessage: { response?: unknown; @@ -113,8 +120,8 @@ const utils = { ) { clearInterval(focusInterval); browser.tabs.onRemoved.removeListener(onRemovedListener); - if (sender.tab.windowId) { - return browser.windows.remove(sender.tab.windowId).then(() => { + if (sender.tab.id) { + return browser.tabs.remove(sender.tab.id).then(() => { // in the future actual "remove" (closing prompt) will be moved to component for i.e. budget flow // https://github.com/getAlby/lightning-browser-extension/issues/1197 if (responseMessage.error) { From 797592862455f3fb216e8e5bb9fa5df902fcffe7 Mon Sep 17 00:00:00 2001 From: lisabaut Date: Fri, 6 Jan 2023 19:14:14 +0100 Subject: [PATCH 138/141] feat(kollider): support connector with fiat as base currency (#1774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add kollider connector * feat(kollider): fix invoices * feat: render Kollider connector route in dev mode * feat: render Kollider Add Account properly * feat: pass currency from connector to account data * feat: add getFormattedInCurrency to SettingsContext * feat: render account balance account currency * feat: do not show fiat in AccountMenu if already fiat * fix: kollider getinvoices * refactor: extract btc sats convertion * refactor: dedicated type for kollider currencies * feat: send payment * feat: make payment * fix: ensure to display BTC in sats * refactor: better comments * fix: translations after rebase * fix: correct translations * fix: import msg after rebase * refactor: better comments * refactor: round sats values for kollider * refactor: align api params for makePayment * fix: double slash in api request * refactor: change API url * fix: settled data works now * feat: make checkpayment work * feat: enable Kollider for prod * fix: filter invoices by account ID * refactor: replace kollider image * refactor: use sats helper for notifications * style: imports on autosave * refactor: do not show synthetic in label * feat: store currency in account name * fix: account info always returns currency * feat: test conenction with formatted values * refactor: put currency in account name in brackets * fix: signup Co-authored-by: Michael Bumann Co-authored-by: René Aaron --- src/app/components/AccountMenu/index.tsx | 4 +- src/app/context/AccountContext.tsx | 46 +- src/app/context/SettingsContext.tsx | 23 +- src/app/router/connectorRoutes.tsx | 9 + .../screens/Onboard/TestConnection/index.tsx | 21 +- .../screens/Options/TestConnection/index.tsx | 18 +- .../connectors/ConnectKollider/index.tsx | 154 +++++++ src/common/constants.ts | 4 + src/common/lib/api.ts | 25 +- src/common/utils/currencyConvert.ts | 29 +- .../actions/accounts/__tests__/info.test.ts | 2 +- .../actions/accounts/info.ts | 5 +- .../actions/cache/getCurrencyRate.ts | 3 +- .../connectors/connector.interface.ts | 3 + .../background-script/connectors/index.ts | 2 + .../background-script/connectors/kollider.ts | 418 ++++++++++++++++++ .../background-script/events/notifications.ts | 24 +- src/i18n/locales/en/translation.json | 20 + src/types.ts | 5 +- static/assets/icons/kollider.png | Bin 0 -> 116456 bytes 20 files changed, 742 insertions(+), 73 deletions(-) create mode 100644 src/app/screens/connectors/ConnectKollider/index.tsx create mode 100644 src/extension/background-script/connectors/kollider.ts create mode 100644 static/assets/icons/kollider.png diff --git a/src/app/components/AccountMenu/index.tsx b/src/app/components/AccountMenu/index.tsx index 0434dcdee3..913f14984c 100644 --- a/src/app/components/AccountMenu/index.tsx +++ b/src/app/components/AccountMenu/index.tsx @@ -90,10 +90,10 @@ function AccountMenu({ showOptions = true }: Props) { {title || }

- {balancesDecorated.satsBalance ? ( + {balancesDecorated.accountBalance ? (

- {balancesDecorated.satsBalance} + {balancesDecorated.accountBalance} {!!balancesDecorated.fiatBalance && ( diff --git a/src/app/context/AccountContext.tsx b/src/app/context/AccountContext.tsx index ba3e585df2..d77f28beb2 100644 --- a/src/app/context/AccountContext.tsx +++ b/src/app/context/AccountContext.tsx @@ -14,14 +14,15 @@ import type { AccountInfo } from "~/types"; interface AccountContextType { account: { - id: string; - name?: string; - alias?: string; - balance?: number; + id: AccountInfo["id"]; + name?: AccountInfo["name"]; + alias?: AccountInfo["alias"]; + balance?: AccountInfo["balance"]; + currency?: AccountInfo["currency"]; } | null; balancesDecorated: { fiatBalance: string; - satsBalance: string; + accountBalance: string; }; loading: boolean; unlock: (user: string, callback: VoidFunction) => Promise; @@ -45,15 +46,17 @@ export function AccountProvider({ children }: { children: React.ReactNode }) { isLoading: isLoadingSettings, settings, getFormattedFiat, - getFormattedSats, + getFormattedInCurrency, } = useSettings(); const [account, setAccount] = useState(null); const [loading, setLoading] = useState(true); - const [satsBalance, setSatBalance] = useState(""); + const [accountBalance, setAccountBalance] = useState(""); const [fiatBalance, setFiatBalance] = useState(""); - const showFiat = !isLoadingSettings && settings.showFiat && !loading; + const isSatsAccount = account?.currency === "BTC"; // show fiatValue only if the base currency is not already fiat + const showFiat = + !isLoadingSettings && settings.showFiat && !loading && isSatsAccount; const unlock = (password: string, callback: VoidFunction) => { return api.unlock(password).then((response) => { @@ -82,28 +85,26 @@ export function AccountProvider({ children }: { children: React.ReactNode }) { [getFormattedFiat] ); - const updateSatValue = (amount: number) => { - const sats = getFormattedSats(amount); - setSatBalance(sats); + const updateAccountBalance = ( + amount: number, + currency?: AccountInfo["currency"] + ) => { + const balance = getFormattedInCurrency(amount, currency); + setAccountBalance(balance); }; const fetchAccountInfo = async (options?: { accountId?: string }) => { const id = options?.accountId || account?.id; if (!id) return; - const callback = (account: AccountInfo) => { - setAccount(account); - - updateSatValue(account.balance); - - if (!isLoadingSettings && settings.showFiat) { - updateFiatValue(account.balance); - } + const callback = (accountRes: AccountInfo) => { + setAccount(accountRes); + updateAccountBalance(accountRes.balance, accountRes.currency); }; const accountInfo = await api.swr.getAccountInfo(id, callback); - return { ...accountInfo, fiatBalance, satsBalance }; + return { ...accountInfo, fiatBalance, accountBalance }; }; // Invoked only on on mount. @@ -146,14 +147,15 @@ export function AccountProvider({ children }: { children: React.ReactNode }) { // Listen to language change useEffect(() => { - !!account?.balance && updateSatValue(account?.balance); + !!account?.balance && + updateAccountBalance(account.balance, account.currency); // eslint-disable-next-line react-hooks/exhaustive-deps }, [settings.locale]); const value = { account, balancesDecorated: { - satsBalance, + accountBalance, fiatBalance, }, fetchAccountInfo, diff --git a/src/app/context/SettingsContext.tsx b/src/app/context/SettingsContext.tsx index 02e24c4b89..b2d7690af3 100644 --- a/src/app/context/SettingsContext.tsx +++ b/src/app/context/SettingsContext.tsx @@ -3,12 +3,13 @@ import i18n from "i18next"; import { useState, useEffect, createContext, useContext, useRef } from "react"; import { toast } from "react-toastify"; import { getTheme } from "~/app/utils"; -import { CURRENCIES } from "~/common/constants"; +import { CURRENCIES, ACCOUNT_CURRENCIES } from "~/common/constants"; import api from "~/common/lib/api"; import { getFormattedFiat as getFormattedFiatUtil, getFormattedSats as getFormattedSatsUtil, getFormattedNumber as getFormattedNumberUtil, + getFormattedCurrency as getFormattedCurrencyUtil, } from "~/common/utils/currencyConvert"; import { DEFAULT_SETTINGS } from "~/extension/background-script/state"; import type { SettingsStorage } from "~/types"; @@ -20,6 +21,10 @@ interface SettingsContextType { getFormattedFiat: (amount: number | string) => Promise; getFormattedSats: (amount: number | string) => string; getFormattedNumber: (amount: number | string) => string; + getFormattedInCurrency: ( + amount: number | string, + currency?: ACCOUNT_CURRENCIES + ) => string; } type Setting = Partial; @@ -107,6 +112,21 @@ export const SettingsProvider = ({ return getFormattedNumberUtil({ amount, locale: settings.locale }); }; + const getFormattedInCurrency = ( + amount: number | string, + currency = "BTC" as ACCOUNT_CURRENCIES + ) => { + if (currency === "BTC") { + return getFormattedSats(amount); + } + + return getFormattedCurrencyUtil({ + amount, + locale: settings.locale, + currency, + }); + }; + // update locale on every change useEffect(() => { i18n.changeLanguage(settings.locale); @@ -127,6 +147,7 @@ export const SettingsProvider = ({ getFormattedFiat, getFormattedSats, getFormattedNumber, + getFormattedInCurrency, settings, updateSetting, isLoading, diff --git a/src/app/router/connectorRoutes.tsx b/src/app/router/connectorRoutes.tsx index e37fd1795e..b15382076d 100644 --- a/src/app/router/connectorRoutes.tsx +++ b/src/app/router/connectorRoutes.tsx @@ -2,6 +2,7 @@ import ConnectBtcpay from "@screens/connectors/ConnectBtcpay"; import ConnectCitadel from "@screens/connectors/ConnectCitadel"; import ConnectEclair from "@screens/connectors/ConnectEclair"; import ConnectGaloy, { galoyUrls } from "@screens/connectors/ConnectGaloy"; +import ConnectKollider from "@screens/connectors/ConnectKollider"; import ConnectLnbits from "@screens/connectors/ConnectLnbits"; import ConnectLnd from "@screens/connectors/ConnectLnd"; import ConnectLndHub from "@screens/connectors/ConnectLndHub"; @@ -18,6 +19,7 @@ import core_ln from "/static/assets/icons/core_ln.svg"; import eclair from "/static/assets/icons/eclair.jpg"; import galoyBitcoinBeach from "/static/assets/icons/galoy_bitcoin_beach.png"; import galoyBitcoinJungle from "/static/assets/icons/galoy_bitcoin_jungle.png"; +import kolliderLogo from "/static/assets/icons/kollider.png"; import lnbits from "/static/assets/icons/lnbits.png"; import lnd from "/static/assets/icons/lnd.png"; import lndhubBlueWallet from "/static/assets/icons/lndhub_bluewallet.png"; @@ -58,6 +60,13 @@ function getConnectorRoutes() { title: i18n.t("translation:choose_connector.lndhub_go.title"), logo: lndhubGo, }, + { + path: "kollider", + element: , + title: i18n.t("translation:choose_connector.kollider.title"), + description: i18n.t("translation:choose_connector.kollider.description"), + logo: kolliderLogo, + }, { path: "lnd-hub-bluewallet", element: , diff --git a/src/app/screens/Onboard/TestConnection/index.tsx b/src/app/screens/Onboard/TestConnection/index.tsx index b742981a78..7f1946a9ce 100644 --- a/src/app/screens/Onboard/TestConnection/index.tsx +++ b/src/app/screens/Onboard/TestConnection/index.tsx @@ -1,23 +1,25 @@ import Button from "@components/Button"; import Card from "@components/Card"; import Loading from "@components/Loading"; -import React, { useState, useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useSettings } from "~/app/context/SettingsContext"; import api from "~/common/lib/api"; import msg from "~/common/lib/msg"; +import type { AccountInfo } from "~/types"; export default function TestConnection() { const [accountInfo, setAccountInfo] = useState<{ - alias: string; - name: string; - balance: number; + alias: AccountInfo["alias"]; + name: AccountInfo["name"]; + balance: AccountInfo["balance"]; + currency: AccountInfo["currency"]; }>(); const [errorMessage, setErrorMessage] = useState(""); const [loading, setLoading] = useState(false); - const { getFormattedSats } = useSettings(); + const { getFormattedInCurrency } = useSettings(); const { t } = useTranslation("translation", { keyPrefix: "welcome.test_connection", @@ -40,11 +42,11 @@ export default function TestConnection() { const response = await api.getAccountInfo(); const name = response.name; const { alias } = response.info; - const { balance: resBalance } = response.balance; + const { balance: resBalance, currency } = response.balance; const balance = typeof resBalance === "number" ? resBalance : parseInt(resBalance); - setAccountInfo({ alias, balance, name }); + setAccountInfo({ alias, balance, name, currency }); } catch (e) { const message = e instanceof Error ? `(${e.message})` : ""; console.error(message); @@ -104,7 +106,10 @@ export default function TestConnection() { alias={`${accountInfo.name} - ${accountInfo.alias}`} satoshis={ typeof accountInfo.balance === "number" - ? getFormattedSats(accountInfo.balance) + ? getFormattedInCurrency( + accountInfo.balance, + accountInfo.currency + ) : "" } /> diff --git a/src/app/screens/Options/TestConnection/index.tsx b/src/app/screens/Options/TestConnection/index.tsx index be7bd1ede8..c9a46118dd 100644 --- a/src/app/screens/Options/TestConnection/index.tsx +++ b/src/app/screens/Options/TestConnection/index.tsx @@ -1,7 +1,7 @@ import Button from "@components/Button"; import Card from "@components/Card"; import Loading from "@components/Loading"; -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useAccount } from "~/app/context/AccountContext"; @@ -9,16 +9,18 @@ import { useAccounts } from "~/app/context/AccountsContext"; import { useSettings } from "~/app/context/SettingsContext"; import api from "~/common/lib/api"; import msg from "~/common/lib/msg"; +import type { AccountInfo } from "~/types"; export default function TestConnection() { - const { getFormattedSats } = useSettings(); + const { getFormattedInCurrency } = useSettings(); const auth = useAccount(); const { getAccounts } = useAccounts(); const [accountInfo, setAccountInfo] = useState<{ - alias: string; - name: string; - balance: number; + alias: AccountInfo["alias"]; + name: AccountInfo["name"]; + balance: AccountInfo["balance"]; + currency: AccountInfo["currency"]; }>(); const [errorMessage, setErrorMessage] = useState(""); const [loading, setLoading] = useState(false); @@ -50,6 +52,7 @@ export default function TestConnection() { setAccountInfo({ alias: accountInfo.alias, balance: accountInfo.balance, + currency: accountInfo.currency, name: accountInfo.name, }); } @@ -121,7 +124,10 @@ export default function TestConnection() { alias={`${accountInfo.name} - ${accountInfo.alias}`} satoshis={ typeof accountInfo.balance === "number" - ? getFormattedSats(accountInfo.balance) + ? getFormattedInCurrency( + accountInfo.balance, + accountInfo.currency + ) : "" } /> diff --git a/src/app/screens/connectors/ConnectKollider/index.tsx b/src/app/screens/connectors/ConnectKollider/index.tsx new file mode 100644 index 0000000000..819cd88caa --- /dev/null +++ b/src/app/screens/connectors/ConnectKollider/index.tsx @@ -0,0 +1,154 @@ +import ConnectorForm from "@components/ConnectorForm"; +import Select from "@components/form/Select"; +import TextField from "@components/form/TextField"; +import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast"; +import { useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import { ACCOUNT_CURRENCIES } from "~/common/constants"; +import msg from "~/common/lib/msg"; + +type Currency = { + value: ACCOUNT_CURRENCIES; + label: string; +}; + +const supportedCurrencies: Currency[] = [ + { + value: "BTC", + label: "BTC (sats)", + }, + { + value: "EUR", + label: "EUR", + }, + { + value: "USD", + label: "USD", + }, +]; + +export default function ConnectKollidier() { + const navigate = useNavigate(); + const { t } = useTranslation("translation", { + keyPrefix: `choose_connector.kollider`, + }); + const [formData, setFormData] = useState({ + username: "", + password: "", + currency: "BTC", + }); + const [loading, setLoading] = useState(false); + + function handleChange( + event: React.ChangeEvent + ) { + setFormData({ + ...formData, + [event.target.name]: event.target.value.trim(), + }); + } + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + setLoading(true); + const account = { + name: `Kollider (${formData.currency.toUpperCase()})`, + config: { + username: formData.username, + password: formData.password, + currency: formData.currency, + }, + connector: "kollider", + }; + + try { + const validation = await msg.request("validateAccount", account); + + if (validation.valid) { + const addResult = await msg.request("addAccount", account); + if (addResult.accountId) { + await msg.request("selectAccount", { + id: addResult.accountId, + }); + navigate("/test-connection"); + } + } else { + console.error(validation); + toast.error( + + ); + } + } catch (e) { + console.error(e); + let message = t("errors.connection_failed"); + if (e instanceof Error) { + message += `\n\n${e.message}`; + } + toast.error(message); + } + setLoading(false); + } + + return ( + , + ]} + /> + } + submitLoading={loading} + submitDisabled={formData.username === "" || formData.password === ""} + onSubmit={handleSubmit} + > +

+ +
+
+ +
+
+

+ {t("currency.label")} +

+ +
+ + ); +} diff --git a/src/common/constants.ts b/src/common/constants.ts index 81ee577e0c..343ce81395 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -6,6 +6,10 @@ export const ABORT_PROMPT_ERROR = "Prompt was closed"; export const USER_REJECTED_ERROR = "User rejected"; +// Currently only relevant for connector Kollider +// all other connectors fall back to BTC +export type ACCOUNT_CURRENCIES = "EUR" | "USD" | "BTC"; + // Supported currencies by Alby API, Coindesk and yadio // FYI: yadio is i.e. not supporting "ISK", maybe more? // https://github.com/AryanJ-NYC/bitcoin-conversion/blob/master/src/index.ts#L143 diff --git a/src/common/lib/api.ts b/src/common/lib/api.ts index 1da7361ed6..246b604ee5 100644 --- a/src/common/lib/api.ts +++ b/src/common/lib/api.ts @@ -1,3 +1,4 @@ +import { ACCOUNT_CURRENCIES } from "~/common/constants"; import { ConnectPeerArgs, ConnectPeerResponse, @@ -5,17 +6,17 @@ import { MakeInvoiceResponse, } from "~/extension/background-script/connectors/connector.interface"; import type { - Accounts, AccountInfo, - NodeInfo, + Accounts, Allowance, - SettingsStorage, - MessageInvoices, DbPayment, + Invoice, + LnurlAuthResponse, + MessageInvoices, MessageLnurlAuth, MessageSettingsSet, - LnurlAuthResponse, - Invoice, + NodeInfo, + SettingsStorage, } from "~/types"; import { @@ -26,7 +27,7 @@ import { import msg from "./msg"; export interface AccountInfoRes { - balance: { balance: string | number }; + balance: { balance: string | number; currency: ACCOUNT_CURRENCIES }; currentAccountId: string; info: { alias: string; pubkey?: string }; name: string; @@ -69,11 +70,17 @@ export const swrGetAccountInfo = async ( getAccountInfo() .then((response) => { const { alias } = response.info; - const { balance: resBalance } = response.balance; + const { balance: resBalance, currency } = response.balance; const name = response.name; const balance = typeof resBalance === "number" ? resBalance : parseInt(resBalance); // TODO: handle amounts - const account = { id, name, alias, balance }; + const account = { + id, + name, + alias, + balance, + currency: currency || "BTC", // set default currency for every account + }; storeAccounts({ ...accountsCache, [id]: account, diff --git a/src/common/utils/currencyConvert.ts b/src/common/utils/currencyConvert.ts index 9e836fd7f4..02d9f2b624 100644 --- a/src/common/utils/currencyConvert.ts +++ b/src/common/utils/currencyConvert.ts @@ -3,21 +3,36 @@ */ import i18n from "~/i18n/i18nConfig"; -import type { CURRENCIES } from "../constants"; +import type { CURRENCIES, ACCOUNT_CURRENCIES } from "../constants"; -export const getFormattedFiat = (params: { +export const numSatsInBtc = 100_000_000; + +export const getSatsToBTC = (sats: string | number) => + Number(sats) / numSatsInBtc; + +export const getBTCToSats = (btc: string | number) => + Number(btc) * numSatsInBtc; + +export const getFormattedCurrency = (params: { amount: number | string; - rate: number; - currency: CURRENCIES; + currency: CURRENCIES | ACCOUNT_CURRENCIES; locale: string; }) => { - const fiatValue = Number(params.amount) * params.rate; - const l = (params.locale || "en").toLowerCase().replace("_", "-"); return new Intl.NumberFormat(l || "en", { style: "currency", currency: params.currency, - }).format(fiatValue); + }).format(Number(params.amount)); +}; + +export const getFormattedFiat = (params: { + amount: number | string; + rate: number; + currency: CURRENCIES; + locale: string; +}) => { + const fiatValue = Number(params.amount) * params.rate; + return getFormattedCurrency({ ...params, amount: fiatValue }); }; export const getFormattedNumber = (params: { diff --git a/src/extension/background-script/actions/accounts/__tests__/info.test.ts b/src/extension/background-script/actions/accounts/__tests__/info.test.ts index 512aed3d7a..89a284d50e 100644 --- a/src/extension/background-script/actions/accounts/__tests__/info.test.ts +++ b/src/extension/background-script/actions/accounts/__tests__/info.test.ts @@ -40,7 +40,7 @@ describe("account info", () => { currentAccountId: "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e", name: "Alby", info: { alias: "getalby.com" }, - balance: { balance: 0 }, + balance: { balance: 0, currency: "BTC" }, }; expect(await infoAccount(message)).toStrictEqual({ diff --git a/src/extension/background-script/actions/accounts/info.ts b/src/extension/background-script/actions/accounts/info.ts index e88b6d1f32..04a0bc21de 100644 --- a/src/extension/background-script/actions/accounts/info.ts +++ b/src/extension/background-script/actions/accounts/info.ts @@ -20,7 +20,10 @@ const info = async (message: MessageAccountInfo) => { currentAccountId: currentAccountId, name: currentAccount.name, info: info.data, - balance: balance.data, + balance: { + balance: balance.data.balance, + currency: balance.data.currency || "BTC", // set default currency for every account + }, }; return { diff --git a/src/extension/background-script/actions/cache/getCurrencyRate.ts b/src/extension/background-script/actions/cache/getCurrencyRate.ts index c84a681ba1..b75ddf562d 100644 --- a/src/extension/background-script/actions/cache/getCurrencyRate.ts +++ b/src/extension/background-script/actions/cache/getCurrencyRate.ts @@ -3,6 +3,7 @@ import dayjs from "dayjs"; import isSameOrBefore from "dayjs/plugin/isSameOrBefore"; import browser from "webextension-polyfill"; import type { CURRENCIES } from "~/common/constants"; +import { numSatsInBtc } from "~/common/utils/currencyConvert"; import state from "~/extension/background-script/state"; import type { MessageCurrencyRateGet } from "~/types"; @@ -14,8 +15,6 @@ interface CurrencyRate { timestamp?: number; } -export const numSatsInBtc = 100_000_000; - const storeCurrencyRate = async ({ rate, currency }: CurrencyRate) => { const currencyRate: CurrencyRate = { currency, diff --git a/src/extension/background-script/connectors/connector.interface.ts b/src/extension/background-script/connectors/connector.interface.ts index ba509cea5d..de79899db2 100644 --- a/src/extension/background-script/connectors/connector.interface.ts +++ b/src/extension/background-script/connectors/connector.interface.ts @@ -1,3 +1,5 @@ +import { ACCOUNT_CURRENCIES } from "~/common/constants"; + export interface WebLNNode { alias: string; pubkey?: string; @@ -43,6 +45,7 @@ export type GetInfoResponse = { export type GetBalanceResponse = { data: { balance: number; + currency?: ACCOUNT_CURRENCIES; }; }; diff --git a/src/extension/background-script/connectors/index.ts b/src/extension/background-script/connectors/index.ts index 8c52847a82..6c86aa52db 100644 --- a/src/extension/background-script/connectors/index.ts +++ b/src/extension/background-script/connectors/index.ts @@ -2,6 +2,7 @@ import Citadel from "./citadel"; import Commando from "./commando"; import Eclair from "./eclair"; import Galoy from "./galoy"; +import Kollider from "./kollider"; import LnBits from "./lnbits"; import Lnd from "./lnd"; import LndHub from "./lndhub"; @@ -30,6 +31,7 @@ const connectors = { citadel: Citadel, nativecitadel: NativeCitadel, commando: Commando, + kollider: Kollider, }; export default connectors; diff --git a/src/extension/background-script/connectors/kollider.ts b/src/extension/background-script/connectors/kollider.ts new file mode 100644 index 0000000000..7a1bd6723a --- /dev/null +++ b/src/extension/background-script/connectors/kollider.ts @@ -0,0 +1,418 @@ +import type { AxiosResponse } from "axios"; +import axios, { AxiosRequestConfig, Method } from "axios"; +import Hex from "crypto-js/enc-hex"; +import sha256 from "crypto-js/sha256"; +import { ACCOUNT_CURRENCIES } from "~/common/constants"; +import { getBTCToSats, getSatsToBTC } from "~/common/utils/currencyConvert"; +import HashKeySigner from "~/common/utils/signer"; + +import Connector, { + CheckPaymentArgs, + CheckPaymentResponse, + ConnectorInvoice, + ConnectPeerResponse, + GetBalanceResponse, + GetInfoResponse, + GetInvoicesResponse, + KeysendArgs, + MakeInvoiceArgs, + MakeInvoiceResponse, + SendPaymentArgs, + SendPaymentResponse, + SignMessageArgs, + SignMessageResponse, +} from "./connector.interface"; + +const API_URL = "https://kollider.me/api"; + +// Currently the same as ACCOUNT_CURRENCIES, this is for the future when more connectors support Fiat Accounts +type KolliderCurrencies = Extract; + +interface Config { + username: string; + password: string; + currency: KolliderCurrencies; +} + +interface KolliderAccount { + account_id: string; + balance: string; + currency: KolliderCurrencies; + account_type: string; +} + +const defaultHeaders = { + Accept: "application/json", + "Access-Control-Allow-Origin": "*", + "Content-Type": "application/json", +}; + +export default class Kollider implements Connector { + config: Config; + access_token?: string; + access_token_created?: number; + refresh_token?: string; + refresh_token_created?: number; + noRetry?: boolean; + accounts: Record | undefined; + currency: KolliderCurrencies; + currentAccountId: string | null; + + constructor(config: Config) { + this.config = config; + this.currency = config.currency; + this.currentAccountId = null; + } + + async init() { + await this.authorize(); + await this.loadAccounts(); + this.currentAccountId = await this.findAccountId(this.config.currency); + } + + unload() { + return Promise.resolve(); + } + + // not yet implemented + async connectPeer(): Promise { + console.error( + `${this.constructor.name} does not implement the connectPeer call` + ); + throw new Error("Not yet supported with the currently used account."); + } + + async getInvoices(): Promise { + const data = await this.request< + { + account_id: string; + add_index: number; + created_at: number; + currency: KolliderCurrencies; + expiry: number; + fees: null; // FIXME! Why is this null? + incoming: boolean; + owner: number; + payment_hash: string; + payment_request: string; + reference: string; + settled: boolean; + settled_date: number; + target_account_currency: KolliderCurrencies; + uid: number; + value: number; + value_msat: number; + }[] + >("GET", "/getuserinvoices", undefined); + + const invoices: ConnectorInvoice[] = data + .filter((i) => i.incoming) + .filter((i) => i.account_id === this.currentAccountId) + .map( + (invoice, index): ConnectorInvoice => ({ + id: `${invoice.payment_hash}-${index}`, + memo: invoice.reference, + preimage: "", // lndhub doesn't support preimage (yet) + settled: invoice.settled, + settleDate: invoice.settled_date, + totalAmount: `${invoice.value}`, + type: "received", + }) + ) + .sort((a, b) => { + return b.settleDate - a.settleDate; + }); + + return { + data: { + invoices, + }, + }; + } + + async getInfo(): Promise { + return { data: { alias: `Kollider (${this.currency})` } }; + } + + async getBalance(): Promise { + await this.loadAccounts(); + const account = await this.findAccount(this.currency); + + if (!account) { + throw new Error("Account not found"); + } + + let balance = account.balance; + + if (account.currency === "BTC") { + balance = Math.round(getBTCToSats(account.balance)).toString(); + } + + return { + data: { + balance: parseFloat(balance), + currency: account.currency, + }, + }; + } + + async sendPayment(args: SendPaymentArgs): Promise { + const data = await this.request<{ + req_id: string; + payment_hash: string; + uid: number; + success: boolean; + currency: KolliderCurrencies; // => current account's currency + payment_request: string; + amount: null | { + value: string; + currency: KolliderCurrencies; // => Should be BTC, cause Alby sends only sats + }; + fees: null | { + value: string; + currency: KolliderCurrencies; // BTC, + }; + rate: null | { + value: string; + quote: KolliderCurrencies; // => Should be BTC, cause Alby sends only sats + base: KolliderCurrencies; // => current account´s currency + }; + error: string | null; + payment_preimage: null | string; + destination: null; + description: null; + }>("POST", "/payinvoice", { + payment_request: args.paymentRequest, + currency: this.config.currency, + }); + + if (data.error) { + throw new Error(data.error); + } + + const amountInSats = getBTCToSats(data.amount?.value || 0).toString(); + const feesInSats = getBTCToSats(data.fees?.value || 0).toString(); + + const payment_route = { + total_amt: parseFloat(amountInSats), + total_fees: parseFloat(feesInSats), + }; + + return { + data: { + preimage: data.payment_preimage || "", + paymentHash: data.payment_hash, + route: payment_route, + }, + }; + } + + async keysend(args: KeysendArgs): Promise { + throw new Error( + "Keysend is not supported with the currently used account." + ); + } + + async checkPayment(args: CheckPaymentArgs): Promise { + const data = await this.request<{ + paid: boolean; + }>("GET", `/checkpayment`, { + payment_hash: args.paymentHash, + }); + + return { + data: { + paid: !!data?.paid, + }, + }; + } + + signMessage(args: SignMessageArgs): Promise { + // make sure we got the config to create a new key + if (!this.config.username || !this.config.password) { + return Promise.reject(new Error("Missing config")); + } + if (!args.message) { + return Promise.reject(new Error("Invalid message")); + } + const message = sha256(args.message).toString(Hex); + const keyHex = sha256( + `kollider://${this.config.username}:${this.config.password}` + ).toString(Hex); + if (!keyHex) { + return Promise.reject(new Error("Could not create key")); + } + const signer = new HashKeySigner(keyHex); + const signedMessageDERHex = signer.sign(message).toDER("hex"); + // make sure we got some signed message + if (!signedMessageDERHex) { + return Promise.reject(new Error("Signing failed")); + } + return Promise.resolve({ + data: { + message: args.message, + signature: signedMessageDERHex, + }, + }); + } + + async makeInvoice(args: MakeInvoiceArgs): Promise { + const amountInBTC = getSatsToBTC(args.amount); + + const data = await this.request<{ + req_id: string; + uid: number; + payment_request: string; + payment_hash: string; + meta: string; // => memo + metadata: null; + amount: null | { + value: string; + currency: string; + }; + rate: null; + currency: KolliderCurrencies; // BTC + target_account_currency: KolliderCurrencies; // => this account's currency + account_id: string; + error: string; + fees: null; + }>("GET", "/addinvoice", { + amount: amountInBTC, + currency: "BTC", // Has to be BTC, Alby sends sats only + target_account_currency: this.currency, + account_id: this.currentAccountId, + meta: args.memo, + }); + + if (data.error) { + throw new Error(data.error); + } + + return { + data: { + paymentRequest: data.payment_request, + rHash: data.payment_hash, + }, + }; + } + + async authorize() { + const { data: authData } = await axios.post( + `${API_URL}/auth`, + { + username: this.config.username, + password: this.config.password, + }, + { + headers: defaultHeaders, + } + ); + + if (authData.error || authData.errors) { + const error = authData.error || authData.errors; + const errMessage = error?.errors?.[0]?.message || error?.[0]?.message; + + console.error(errMessage); + throw new Error("Kollider API error: " + errMessage); + } else { + this.refresh_token = authData.refresh; + this.access_token = authData.token; + this.refresh_token_created = +new Date(); + this.access_token_created = +new Date(); + + return authData; + } + } + + async loadAccounts() { + const response = await this.request<{ + uid: number; + error: string | null; + accounts: Record; + }>("GET", "/balance", undefined); + this.accounts = response.accounts; + } + + async findAccount(currency: string): Promise { + const accountId = await this.findAccountId(currency); + if (accountId && this.accounts) { + return this.accounts[accountId]; + } else { + return null; + } + } + + async findAccountId(currency: string): Promise { + if (!this.accounts) { + await this.loadAccounts(); + } + // guess only for typescript. loadAccounts loads the accounts :) + if (!this.accounts) { + return null; + } + const accounts = this.accounts; // just to use in the find() + const accountIds = Object.keys(this.accounts); + const currentAccountId = accountIds.find((id) => { + return accounts[id].currency === currency; + }); + return currentAccountId || null; + } + + async request( + method: Method, + path: string, + args?: Record + ): Promise { + if (!this.access_token) { + await this.authorize(); + } + + const reqConfig: AxiosRequestConfig = { + method, + url: `${API_URL}${path}`, + responseType: "json", + headers: { + ...defaultHeaders, + Authorization: `${this.access_token}`, + }, + }; + + if (method === "POST") { + reqConfig.data = args; + } else if (args !== undefined) { + reqConfig.params = args; + } + + let data; + + try { + const res = await axios(reqConfig); + data = res.data; + } catch (e) { + console.error(e); + + if (axios.isAxiosError(e)) { + const errResponse = e.response as AxiosResponse; + + if (errResponse?.status === 404) { + const method = path.replace("/", ""); + throw new Error(`${method} not supported by the connected account.`); + } + + if (errResponse?.status === 401) { + try { + await this.authorize(); + } catch (e) { + console.error(e); + if (e instanceof Error) throw new Error(e.message); + } + return this.request(method, path, args); + } + + const errorMessage = `${errResponse?.data.error}\n(${e.message})`; + throw new Error(errorMessage); + } + } + return data; + } +} diff --git a/src/extension/background-script/events/notifications.ts b/src/extension/background-script/events/notifications.ts index ac809ab9ab..fe78da289d 100644 --- a/src/extension/background-script/events/notifications.ts +++ b/src/extension/background-script/events/notifications.ts @@ -1,8 +1,10 @@ -import { getFormattedFiat } from "~/common/utils/currencyConvert"; +import { + getFormattedFiat, + getFormattedSats, +} from "~/common/utils/currencyConvert"; import { getCurrencyRateWithCache } from "~/extension/background-script/actions/cache/getCurrencyRate"; import state from "~/extension/background-script/state"; -import i18n from "~/i18n/i18nConfig"; -import type { PaymentNotificationData, AuthNotificationData } from "~/types"; +import type { AuthNotificationData, PaymentNotificationData } from "~/types"; import { notify } from "./helpers"; @@ -10,10 +12,6 @@ const paymentSuccessNotification = async ( message: "ln.sendPayment.success", data: PaymentNotificationData ) => { - function formatAmount(amount: number) { - return `${amount} ${i18n.t("common:sats", { count: amount })}`; - } - const recipient = data?.origin?.name; const paymentResponseData = data.response; let paymentAmountFiatLocale; @@ -46,15 +44,19 @@ const paymentSuccessNotification = async ( notificationTitle = `${notificationTitle} to »${recipient}«`; } - let notificationMessage = `Amount: ${formatAmount(paymentAmount)}`; + let notificationMessage = `Amount: ${getFormattedSats({ + amount: paymentAmount, + locale, + })}`; if (showFiat) { notificationMessage = `${notificationMessage} (${paymentAmountFiatLocale})`; } - notificationMessage = `${notificationMessage}\nFee: ${formatAmount( - total_fees - )}`; + notificationMessage = `${notificationMessage}\nFee: ${getFormattedSats({ + amount: total_fees, + locale, + })}`; return notify({ title: notificationTitle, diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 9431b35007..db07359438 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -291,6 +291,26 @@ "invalid_jwt": "invalid JWT passed" } }, + "kollider": { + "title": "Kollider", + "description": "Login to your Kollider account", + "page": { + "title": "Connect to your Kollider account", + "description": "Don't have an account already? <0>Sign up now!" + }, + "username": { + "label": "Enter your Kollider username" + }, + "password": { + "label": "Enter your Kollider password" + }, + "currency": { + "label": "Select your currency account" + }, + "errors": { + "connection_failed": "Connection failed. Are you sure the account data is correct?" + } + }, "btcpay": { "title": "BTCPay Server", "page": { diff --git a/src/types.ts b/src/types.ts index adfce13f06..3173a5ac19 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ import { PaymentRequestObject } from "bolt11"; -import { CURRENCIES } from "~/common/constants"; +import { CURRENCIES, ACCOUNT_CURRENCIES } from "~/common/constants"; import connectors from "~/extension/background-script/connectors"; import { ConnectorInvoice, @@ -28,10 +28,9 @@ export interface NodeInfo { export interface AccountInfo { alias: string; balance: number; - fiatBalance?: string; id: string; name: string; - satsBalance?: string; + currency: ACCOUNT_CURRENCIES; } export interface MetaData { diff --git a/static/assets/icons/kollider.png b/static/assets/icons/kollider.png new file mode 100644 index 0000000000000000000000000000000000000000..ccb10468230c1e6f087a8bdf11b57b9cda5d0590 GIT binary patch literal 116456 zcma&O2Rzm9|31#?WMmbU%4%^MR@rh6y`{2ALYXOLo@3=Wc8ZoHMI0QZ5JDkjQ&cjK zm3{2(;MiyU@7JmK_^jXm+v8E4@jB;z-S_<**L6Lg_qlImaE@o&zHKZlEIiuh&s<<( z+42MYeaXQFUODuKF27`|}s{Sy*rqEG)rySXef|OTiN? zEZ(v#EK{~DEUGV9SOhT%pN!SO8#k|;Ydh%cvm67DIauJVY%E*ABUTm|_=o@ZV^;9^ z$ltGrJF>u;*RZgl!2jTGl3D-0G8y*gyIX!FZ~5!-OX$L<3F1*KEU*#Bi{@VD`g*GN zXjf^wYv`-jrLnFU=mHistSWfudfm%zKi1X7%~KVNJjlF46+DJsmN~eed5M=Z@}RlC z(SA*|$MyY+(z4RB2T|Mh@87THam_*X!kM$bKMtND58m|h!l=r~`1trp`^ZV7J#NUH zP*G8lkv%DM@}v~FLdw(E&C3oe<>q{^hm5Z|~Rj7p{AvUA&=Ln7TQ7p-%q($iKb(@1HXCaJ&wd1^U=u zqx|!=f4|=7CfW-Pw#dWLUfa#M# zD1qGNvQ&^2zSTtC)&z!!9Wt}PGdf{bZA$r-gkJcnx&CR`_VYFjm zA0nP>f880+N~}id9hSXw+a4qv3$)mJkazpvt`jb%l3A?JUp>zqx#KFgiZwsdNnQo9 zCA>DC1(KIEIWG#a?|eD$(VV zKDaCye#}5aXzT$&*un&+V~gda&W&_%5tjBw27Y(>$S@^-Q=^_*{}C+>J4v99^+j(I z0+%)m98ur24uQ`qhuq4eW|B4*CbpsodXFWd*U@+D2iS|+&&>HatGMg~I4W?-rg$OsoL+^8DjwyjN;Ckl zQ#qC!f(&9^;7Iwil;U5TkRSzayD0{L5p2jK*${TrXpaAsw~}5GGVw=}KsVzlhBZ;7 zP9wIpSK?qHvA)xK5WEqqT)ZO`HFOTlGV&^92nF96jgx%SO!&g6Ae;__i5CGkYW+ z;-@~oD`+HoOpxB{h2AJ7pZXRg?eesQR;1wSPs^G(IqiN$5AUFke5qp_E6~;Epwb{~ zd21-rl|gOsrxV4>XD7zhT|G`TF2DKZ4hAqU#JKRT*U+;r5*)RdoH9r_sFLdxY-kMr#fb|m`qN1fHN?G)(h0K+I);!r0D27xOR zWOAaq-)4B=XmPZUSgLTtbI9%N!8`_Qg+p;*i)*(-XQzBp-7nts)uOuGV0@V4SrMW`^xLZ%tQ*~_9_|vcFC4}wERu|)C49}(Uj(7I& zr8h32)9MtM8~^A8>nBek`^_nfU)w(FSw3kvGi$Z_luWW};u9=a$Mq^%!OYw|j*8bf zFSFIVbJjI?hL7euo;|zoNJyVfatAP6k0M@LEO~FZy6<{x0il#MEQr>Pbk2qrXpb(; zt57*6racXsk@lbfVR@P0UsJ^+6K{}am!d|CPDST^d!zfZDx%teJ11GmvgF5ti-p+X zZh1drP7~M`1KT6`>yMQ!9}Y#{3^(jXQQ5^xY1u(5d!v2SzSb!KPt)L>!NI&@KTf~P zPzM%J`=l~QKj{PGO%OFE=Y@#AH9uW%(S}2?nCnaV`8_pJAxm~H=v5ge(wd$`*s|z>FE*-8GLDL&3MPp3Au+j^jYrORyxeWHc z(wrYl&a+;KV;AHtKj5#l{)SZYE%Nlh7yV-|`jpBKpU`Z3rF&fVDsGR+7$1i-ibJo0 z=+fG9hLc+=#Hvd&EiPQFlb*PKjqWpFvPu@4oIjN|c@r#IrL?$)k9SF13=Y_@EjK5% zYSR~h8wD7bQPYTmeIW29!^57i^zrakWvd;7o=HU4 z6P6w{^;FD#LdQ3b+XzZuRk|T}qR`c=SPpwBv3F%TO9s1bprIwz_lwCJO`YUg_^p7v z1P4)R`wFpa)1cLmOv2jz@V*6mG5tR`|0>LjUHQ!BVit3~*7~lvm6416BK=of${G4E z#~I?5rPb`#gjar;$q(#VxsOT9SxN<2IhZ3Z@iIu-*lGn$)bETOxH%_XTNnKGMs>@~ z>^nAy^|W*0oq^QBL+FiVjLVnj5zl{3Xi7JDjyffWILF*A-NMqL_kt_DCrvXpZpWYE^+sMUnV=+Y+>z4{dx9P9DllW$O&`|5pl>Rfidl`y8hm8u3}ZJ*FTni z!zMlw+WIyEOW%nO^d(;4n0xeL#LPnfC6m>a)d~fF`=L1E`Ze@hPnRQ;C)r%?mvIN5 zVnLRQ7Vb#xmX}da`Qw?!F3y{IGwM8~Pu?~lZq+!4hpT(^os*_#H}PjcK9VlO`pG1# zDM1+IK-p|K3RhpKMu}pN&cQdP4e(1F6iRv%T!it%Y|b){Y*#LSfra^rBCQM>;y?Db za`*D$Fn9vGE!Fp^9;=RR2^h-~tW)jK^O?@ayyNK2PBHo_gZef)$opLuzb2H)bIafp zysmx{7a^y2M_?wG%Gccmv+)5}iuRXDQa8z8!mVzYyo@-c%$A~yud1>Z-D5qv(cz43 zPW4@n+xZIq@oBJPeOe^avIeyWb$I67T*+#8&vM#Z0|H?uSS=R`)=$TK^-h~IBd{cY z(FVeLLEMk{v(Qi?@5bGSE#D%o?3XSVoe3#4e;)uhz}a$k;&1!0A@kiyNy{%Ut)0U= z%&Y87g)6=QD9f)W?!DPg3d&PLEZCN#MwpnIjsi8p)&nTjihaEWF`ZmaZA>HzLgn@u~ zZZSOH!rG>T^we*sbZ-R)>qQw0?UAADnC~?sEt|IJFs+FQ+%hCh;K4g+$CUM9J}CMw zr9KLoMlOs-1SwczQR11)Zjr&>Mb|=fcLhg@?pg5A#7wudKdkUwRiJ%)+iP;fl9Lhz zW)$!mSL#rdN=r+rN$F(N@eQmJ4HHUfPC?J}KR}t@)*I%vS{R?6!W=m7Fu77Na5&An zFlc)Pp86oyqI}_6MD-{;zD?jR&AMjavbEH)hc8LS0imH~YAo~I-9?vAi}5-WOA|hA z7jM>ubxW@DPq)4zA#S2%2_j|djk-RJL_>NFVeK>jX(ql)9*0k4zZ=)NYz+*;SRAv? zWxcRL^Z|09a5HI+=W(OY@OTi-VyevPN-#|F~?8H)m+IzNxsw1 zx`Dk5{{;xlGx-<9e6Myol0&l6e-cWk*gqbl$ z>KQ-!`{|jQY&N^3Wf7fw^0Z&y2__g#PJG*5Dv-XH)h0#MS)!oyXwgNn7l`w+n9ekG zV48(`QX>4_TYhsxFvA=~U0yc7bR1t8ABSIEdp%z{eIv4bbhYWzV=v|^W^*=+TsFIp zWj=WWPc>sul#l^vRhkg-8|A3(ITIHA8C907n(LgsNz2wE#~i~3p9a~6O&V5OMOp)N z;P6ssP!0H&e-fo5lPC5^4OwKIP=3psUYJudJT#x>VVz`*(y46Ows+u39~h?hi24Ga za0=g8q2zyVJtmqjJdso(wOuW}15Q`$zN!vj-I zaZC)@GnB3qzVpt!GNYA5q86}=m9OO}*;wrY;jXJ4J|XR5cIBcCSh}p^AvSom>kQqB z2~QhkyrGSVv?!r4Kl|rMi}B!HU0&?#2+hGO%_+C!|5)Y`jl5pYjXt%pP6gpSM~xZI zlOE|-f$zD--|V0wp!Wq+G((>vQM>s>dsk9z-gTvBK_T!7d}7XZ|KsZ>z#1bE%`VRf zl%Fz5`e_3KOYcpc7p$QgJtys#`d-0wHLR!UT@Z_=qSEfoqBRz4j#L~?(l*R?!W}zH-sD4hEOdtd46y_mH@5*k;pU82ym$pnEFNqcRb*5K_hsou z^yF$d-_k~Jt&pOlN}89-LXp-ZiuHZ#<-6T2P2V?OuVSs$9U2;#*0C2@hc+x$j3!bj zJTuGq?xh;k#+lL5EWi;vZi7^dXEcQN43N=b{ST?oki}s4Ts^f+wZV{10xzun7L`yp z6o2~ZcOCIrla*n*Bv~IL$4p0MCN=+N@pZd0QNGxEh~;m0Kuw+p~E2-wpQQC zn-eg9H`3C7e$FQo%7e;inAeS4{cm1F@boLEWx%46nk)JfqZ?z4ZFLv(@+B*}i8{cP zQ$(kq_Gk4NRO<4=a?bM^-Zw*UlU`>ZhM~No!=k)KiT;%&9aClFVZ<%6)wnO}5RaV!Sx^+sp?kLG$ z)pxx8c(^)-CL%bX-ng@^0QgfT2vBcLcePiqtMq>+zh-U$~I30oF+6o>u09dt5E{+l5ezPC z7jt<2@Q$=d*OMKcH524Gy5w_<=qbE+&wUckTpZVw)@k^;Qhc5{s8xqEQBTED`4g~1 z(LR&%4maPFx@9 zslFdXbKma8SsC`=;G(Rme`>_MDut{?Z}>C|S5v*oj3?{*;bG_KJV_63x?vF;>y|-j zaTi)Khe3=~dy3fLb`8@Vt*frcp`pb##ME!qkMx-}=H59BoPXv#4B?}Ds`W`X$aR$x zWcEnE$>qp^A613n(X=2g2C7%o>c_+dTg8jB?tE9y8KrbyqM4_8&b zp@)d~dO2b1z#2Bh{ux@HWf${z6@ZEN7u?7v1Xlat2ZFr3X8l&s6>G$ow~IYiOpG%j zm%jqof+=2M9l(>p1WvsiT3oC;S}~4lv?gF2S#$aLz})21C&Q!U)XEqN`OI<)<|Ioj z#uhnk9PU(i0F%KdNy3?6nsWGb%U0*=`B3_J2br68B@iYmS=4jOj?8tZ`-CrL4)|hv z@buZSjEtwis1y!s_?RhlMX`e&?}*X>L)m=r$z2V@+zv9%6IHyJ{|WHK0$P!GwgGQ= z_{!q5VPEB3-dIJ9aq~b?tDRggND}ssn&FmHVgOmx#0a@@gJg02us<1E@wGIJGtV8} z5q}h+baSep#%x-kpm0S#J(s@=3-dB7Sm-{M4&YL}0G6yD=NV7WkP;Ej#l+o^9+d&QeIVRuhei5_#(hGv>nc@AB?{4%n1J=H8+VB=Ix z#Vk9n;Cgk*#)4qtT;=Y%>J(_+0d*?blkJPS-feZC+A1sx6NAsO^IVGRUprVa*-ZXg zQDxHdP0b$xVtTZ){5~wm_l`?}?a4Wi`K&ge0dhfN`C8 zG95X=T_`42Qo3XPYdRZK^Tf6xmh3O>u9LWL;AeaPWVlj(((-ZVb6f6is30+|2v;Q& z^3%}DfCeX7?8~krs|zw}NG;Uo>|%j6 zXzoHT6%Y#d$FlUPj2@{5kt+Q}$Z?y+Wy5|#L%&+09$_Z1PVvS*{9#pEi1;hv{${p} zbA}`8jzT7wkY=B{0xb5_{OsyHHLU1h7=E-_8#BRa;DDfMH-&il_&TC+HOQ@uF@u05dgS#{?DCC+f)C za5Z1H$?r+-r<|d`Jp+KZ;lS(qWefTn_JZc}>h)lx&TQ1nG%lL;ZV>w`w48 z9S^#FtJ%8f5e_KT0%eR8!5Uc=9VxgL$k_EE@!vpxUCy2q0i*U*5s za+)nc)RdBD&FXYxO%jUK1o)R*=II!k~2%EOs#mqZjD^mEt zz}8{a8G0J{(PM;|mKWPP>j%{1#mYPwAC>66H{&m*=-8eEh(ASS*2wi}h{*ZDDMG+x z?A+{pnLXd>& z@l9RUHNNcYo%U!j)UukOhev`&kv||#duF@~2X3ut5)_NE+QopE$z}n3aMQeY^b+Tc z2W&pR_tR>EOv!?bVdPj255jSCxYO%X>)7?{ykVVFJRdJ4Qq8?jS9tXC_B*$M%+sXB zCcPur^mRC5F-{0r(G^LxiBk>5y?4++HjF9UUAE2V{C-QNAjU*DHD@Mhgr}k_MEU`U zOwU1NI{jSvDUZ19B0O7d)R|DEj$CWOc%XI|+Ya;ZP<7?uU0JLo<2vw{>n}Z^JGTiL zJ|lD{kv_t0x4OO#)qS|-3IOOA5_`8>W{BmIBNZEpf80Ss=A$H9ye;WONXq(%RjimA zwNVHaxbRU<`am2QRtShGfr{p)Zva#_*BNX?^x}y6e0~$oX~anzUc^{Gza8r}MVW^+ zU+VNHjS=Jbq}4?`Ykpj~(){nH^O(jy={H~gx`H_KMcl;`1gO&DEK^4$=N;#9#8`Or zJR^dS(Q$>Kx^C9lk(Y4^#Qo$*_%97UQUn73`0IR;T~nPI;WCS8p;JjAvBL23tw^VHz=j9!+a&F?UgPm|jaLzg zP=X+n5U=HEM|`C}j?&Y~8)+R#T2Z2{To&xO3dYKk0$l%cPyFTc0FXWAR3kqV447K> z6YSro_`OMuSS#L9HFnqFhONk>;1X7(lW)0d+dwMHOh|cswR?+<3PMBSGW_C4bfSZfi1BfT%ZEw$u9 zFf@2x=`0cbb2Z>+XHdPD>UtH1zrX8VgT!3}{m`5v_Soms5uN#Izm3Z9%9S5gRxG3) zzh)@A_k$u*qxM2m?!;M@_`x+T= zZo*u-UDcQ(bJJeM;|g&Ht{;F36#8~6-+>U!TUu933(2Oh zf;OZsdfUeLBCI4qgCy+~d&LZ`W9!K&A9TzqXntNFL6niaw5)iza^l(6OO{dP(dNbF zT_#mQ?{I4e8U4h+L&QfSX06P}nK%=Ei1Gu>%71L)u<%V7Y3Ts{=NtB%zdA4}^TAQO&BHHM zi3E#UmzAroydN8)P;`{)WaUz*BB; z=s!)=-EV0aM5@va&nbeGHd@aY6 z&jzgZj$b%=nZD}pZ&%1OT1fxrMrPmL1 zZ0~W(6M|{qY{*Db$@c+~K=|fhQ8;g!pzD!lrCf!n_YxT+?1L8qY<>4W4(`bl^)^!3 zyYp9IB6xM^RWUi%0B##!w>-iyHgz4$vGGRAA)Wma+q2*Lh>A2+xH49hsL%MJ7+3*@ zy-3^D@mQ+AJ3LqbSi%oAg3$J>U!T8 zbkm5uoD|Kw%uGA0-4FK^onh$!_mci`;mqp^#*i;o zT5dO57Asxb>MjG_Nyx@kYM!-JYnEJ8Ian`{N>@s@S@W3nN)Ngma5;?+*8joP$WMf^$P^!HV!xRP)Ggs00qahbyd6N`h zz7qF&pKN-|{MrM4vpR_22EZnz8Yj)3`~g+lbZzL~k!5Z{i>o#B$AaQ|0!!4*RoO~0 zQz5ZgEdCL=thlztRA^_C_E%NTT12X92k3EuB@!|m5kt0AVy^Vs_H;IlZy3lS?e~2I zYm*gwn_t-vMmi}LSm3)cELO4@Wro~%Oekf{9M^sef5w6^DssM>Q42!nXLvSpR$L}t ze;my-aMW*Yq0o}6_(grHz&@e^gj>Q9~?FeY>D6t8lkx6t zzTI#x+=SCIofdIiM6pJHvP`v>Lm%Q}J7kpLK3i~Yi$B0_h|+XiDJJOz;ub_%rzrN@ z>A@bNSWw#f)UULm-Rr3O3RtFSOZ2--N>4S4N^S6lJ7($#{ys6gt7;Ejy-WO#=~3W@xWcPI;!9Ei zohN47(_&K6Im4fE3U94}C;&hpV$o0vf1v0NtmkoH;LaemD|iRo*|Q354!#!+3*-AR zFw!D@IyN#pu#X9@6)i81KC5gz+)SFgc>`y9NN2F^mo(&AzHwkc2^sHisBE#)0PDK8 zCx6o?0@`ISUs-Icj3G~sN7=%6eaTvgtwtagD{kHw<0(Y4WF<#j@0#SrhH zhg^$&Xz5lp|=R|a2I~z`KS_bBvW4wsZfGa>z8b*cJN(1vJ|Z{wp=)UG+iB%jUG!Z z5!SleBk86A`@6mXYlW_hEW>j)s{T6;2nvT>=kPr;c8=J z)7I*Ey^8D0-Ck2zC2vSXd--o78ta7FN_!PUak;p=@>|8KB?RUAD^Sl`Q&sK<#ir`w zue&w+C9>MTg8VK)Aq1mi#mk^Cm+_U+rR;bT?QZZ~_|$UPsAy@gKCjb7PCaS$VJ$*X zuF~dP%mx1gLr2OZnvagSKio5E{ESVr)zA9vE^eO44JGBS`YknAqWiP7#XYf_lSU=1=n8twOmhx3;Z} z7jRo6z5NLW^Tp`E$?Hn}D<2~Tza2ahvIC(p|HmPN)9)ftAIrE86wqo>3@u&i&7xV> z-{>AzKfOf@@9@B7qNYBQ_V@~Ks(YSr;Em#%r2z_;bwZ2tmqAU#v-IbcEBqX3t>L_p z*+|A9fV=}eo-e=aB+tPo`qJD|JksDB=Lc61{tY~lxF7oJC-HjL9;K*6{$imI!u^t% zv&fFaWu3B6u=qDc7fN$aH(uP%Nk`l^n56+-%0RMnBgR}Zuy(L-Rn%Io5|9A@Un$5^OxU<9Q_qf3gpLhgarqpnz5ubZ2YEI4a;)V^Q73fAY}8C8@`#kO9A1xokh`UHpJN-mt>==%b;C-HFnGe(6QY zO*F*6{(UpQM52+3vosyU5#-9GJCZx@397E1)Ub~!#MYQFm9yynvs+I8)f~ zk?T_xSSe^NFw$Q$WH)%XlMicr#xk_de&YgIC|ww-t@xz6D%IJq(8k|cuv{7Hmq_-B zu|k{DB9I^tug@4(%nfb?odjJk_asY&j`4-==bgx3C=&h1i|lx_;m~&r_7?$9HG%lP zp_aT-Y0`4)Zr{q>e+`7UP&EIO?Wu7?+1}U>7Gv|0L{GG}K-*mq%8=SWnulR>disFG z965}UkE6HMWe1LoMwSmF=4Fh$%{gbhU?Tkiw>||aC_RN|2exw&$_v#4ZeXj_7hhk; zt&h8pA(FK#vKY6zKB3D&d(&m?@LJZmz|E|1DZ+WkJ4jsd6_R|Aop3)(sEY1w7n;Bxmbm`uhF|!BXjst+69N4-}1v znP2kuV1aUr=Y{Ua+W^jyQTtMt-K4HmCPDL3^=;Kx>Lcs=Am`rjjgKny2f`X0eATIY zR^Gwpd3`GJAvVuf8!qt%Q9 zhBFiQWQV(&5ZYEsvuEy8tP0$b7^UQMQaMq7Ay+vIftQ)0r2?+O{XR{78wN|-2YW;m z@S~x*7IPb{Hnq}afeGUqBWmJNq^P7Ldv5Eh+1fgSva^On2_pg6H`@Um)I7D8y2g6stCmN%+lk$jpU%e>!m4H|0#)y}BjY=v82)TFVhR|8SjO+7)D;NvHW-E%TCdnX>lWRj{++#2 z6M_Z9hsFylf98ZQEF72;M82Wn%e%kbuCL=h+a%_>dpJf&G|7utvwW)Ip}FGv1Ue=q z^oysUs9=*LWblo9YPkYfJkfD6I^1?DfMQlV)2_8$`Qh%R=z3>Kd%E(ntvh5d(q_i{3W;X8#P0NX6hl{P#x+LrP z)*jeklRU0Zt}hjdI>e}k0#t#^b34Tvi>*`(krkJngI7=PkS;l=PAx=HYUcwRu6fAM z$+yv@0YunV(wmCw8HI~dr^fwP?s5tC3k2G%NAB8`S5l`eDq%|=gvY_QwBeJ!R!ll; zF$$(4WOCy25mRNEiqfoeY?SoTW#Y@g2p|SAtou0d#O3Z-nL-fG*(%iZ#5ILZs`Zd z+>u#qNPlBSt=TEoNZx%}2g??;AQli66qmyQrV(Vr47;Q3(BCAl+dZlHzv`d5yvw}g6`3KS$67_aq`+qcW zm3`((GDRp^Yz=U4V4Z3a@NN3C^ckstdbnI2ucm`9Hk%yO^YAZvj%u!Vf^tYxTxji$KlD|odd zS!TYR(Ldhj-`O+2*SN>Yn_VC|bCdU0TFfnpkj$n}%k?WOtNkJ<&P@k-?|318%%nIU z_wh0I!QGU>(U^Vdq5)AsIa(0;Doj2%J8^$XNNu6sgsIzk2t0JJ3W~rD->1r49y(X! z%9r28#722j%BX(Hc^c?7K+=iE#^a-<+t5xep5v3qtQIMHo;dJ8+FkKLxXkU(3d&sa zmteynOu`tWi|ynR#&FcJM_q)P!7sbBK7V=f3ahaX+D z@A~@e1A9!?(;A^}rkx{P?Wf#BnA@%oBaJfN*QBIWU@@CQ3_^?iyR$yWLU_&SiBo0` z+{Z&ENuGqw?w{9I`gA`wG_aRF;wgWv%E~>jw!wSZWcqXU!p&qHsHYJ-c6(w3HL&nY zpylA}4L1(Z;1DCQgV(JI*Y>j8!93k2Q?wjJ)XcN)ZR{otF-KGyu#jsd? z@yU`akeh%9&hLe!QNEJN`6h{I!7P{0z5dPhKL?Lrl7$d&%=p{ifcIC*o>AMltr#O$ z&*5AO}-kvR?VK#5&Y8=2L72-!&&N_#p zRNp1=$TZ3ze~x%z-wv_eDXLU`%>u2@H{WAzOxg+Tvc{E@E%Q9u-?w2Wlh-8H;4dB7 z6qX4`n}9T_zUozA=^9T9j&E2zy>? z`IY0)zFzs)_aRL7Sb|u{s15I5eYXLhP4vh4d^q~)>io4JCnsA@e=1nNg&j)$YyISE zs-=BjuhCBHkAwVtFD%+Ycm&+jU+9Za#^q(Q*1E6M?fCce4XP(puT^`*?@EHVU-;%i7f;T=Z6d^#P<1HX^ zcreoPfXbj!LX1-VISQ{fES=KjY>Tcu6to~XpJ zUYcS2I%K9kpIgl|s<=d}Hd_+*_Dy;^R`}MY?Q!HxPzd5uY${j(b_!RHuD)dO|9HK_zgXV$gvSjthZoH%^~aEWO>2iS~}4WLZu{fKo8K^g^&N%udJl1?Qbu?_Npu zC7@MM7iCX9nLYumcI?E*oFr$1N|$)Svt>dYwj$PzqJmwo&X2W_(_^aSx;ccbPosh)x@wM0+F+$x)Q3ld{Ynmi z3M$P(TyJUsf_UPhvWENhIJtopTsb+-(l-5P8tq_>vK8x1 z)iqBR9%KEqE$7wb3;r%^Shx(gdJ>XPJ!!V@$j3az`2C<|1-@n zJ1G)W&ld=uz}yE4EPG_WLxaMUXTj6fhMkrfzna3JTYekq$Xv_GE=D#w z9tbCrZZQD8o}wukrd2J#W%#&r$W)ok=%wOhrGnBgmjgiW*ZVeK%GUY!VP*i!lMs8c zmbRQ`CeU_~#4{Q^#GCHABp^W|naGooe#38m11y=p+nT`=Wi7OFsJcJmcKBX5k~o_3 zSz+x#Hh0dAvbUok4$fyZ0q?SZW7MOD7!h%Pn#9W#t^Z#nftX0!FPmqf*ktj-h@lMU zzNao*TD>&?4zsU}gnI~z){tmc(JA_8xSl<{-8!aQ5{QHs#C) zz&WBtLd_{Lo#(@Wf^o%gY27Ogh4dSBh$EoX5F$zH9a6RU)|AFqm<@BXO-er?501|6 zynlM4C$O?tiuL%1SDxbxg0nv$%~}_AZ1p)4T;hjsQ}`Qy-w@Ro+|Ulv-~}n$&-FZ2 z5blza{}0>+c|bsm!05a?^2ZtC6w?EY(?wL0P_}jINwh(rv5F127rvLv)dm28OFhhL znB9N$!)H&A+Ht;Vpo(reeX{GRXnA_zP}V;p^oI8SQcAwX9OCl3PjBE8-#>}7AIpS; zB4(ew%Xf*qf;}#HPrh z0Y}lf)b+*j3TjMf8CP9JUMZITph*Df6V`(YVmp+r1TQ4_`gPw+JPUxkIkes+2)ZXK zX7=s_t)VLX95QC0q8mkc?G1_sQ&-a5i!Z?FsVZZC$T;`jG_n@m&9?=C{3Z*RIVja> zYrC-p^OjfE=Bg3q)I^oS@%-ozfVDxd0kfzFsa2Qr#`Y~2U)G*<*e2cnZxw};0l7=% ze%U-|Vy{cgYEQ$5Z~0HulMws3wb7*)`1b7Hm^ZZnDl=|5BubQacuORgB*`fQ-tkNW zd*3myG*z-d&x+HlXsYA7xf>{VVb)5oR5&Z*>9Y0+dz`PI(l*iEUn;eOK1_V$DCOAE zIf$UViDJf&n1pAv)r_?M6os4)9my8ISstzVb;`yG6=OhH%{HLobHr&E1F@wz`~`<813*2h2Yw#VFOcRAgf!mk_zBVB*f zu()pDw(U8{2hBPn-(!IMmy06`f1#Xg`fZD)z~BG0#ig(?vu2^}!mi&L=0=fJmF=!c zh&b<}mpBdc4_=tBywqkT3@Z#3?Y;X#Ec_*(6!%qV&G>eMKs+`x%9o`{r+P`%YPQsj zS7SHKCzuhGkzb(9E@N;xyXs)Jj5y{ayjoWqKboh7XxHFBVrj?Tde;SLwGPBm!E$2h zBHMo7V!z(!=C4*SgeMg-d!heU&Y{i~AS<$n+PQO3xp6m#|A4yqhr8osYEm}Xnrv&Vcj;zM!~3qZ)(E!S zCMD3kT61M z!7?Lp+@)0ROJCJ3vg5M#gTlI7Sf+fML#Gu^8X<9zK9SaNtT2phjM#4N)-4&iu}d~- z?3AT(oj{nXNFdCCDBM%kpqZ_tx>YSZJkDEt{<$m;U`BzDd$6w+md$SRL{iM zip;Om?g8E3cdXSxyBgsdRJdXMBG62Pcx6*eCUmW#%odp- zs5c`2tg3wJ_vKpW?(+$PRAyG1E7*MkI0S+pwF36_41V-|1;^R8pz!09_galIPeoON ziqtJr|D`mOs$tvFgR_Q~p?=l7<^BWLD>YOkM^K<@thG9@=N!|DRx@^nP)E3j zDL6>QxKBNe`!Rxw1cQ#dwUEXV%Zsd^8I5s)CGhaHWXZg#6}m<-J&;+gF-H zW3Sv~HkGbkmhX+Rh1!N=JN2p4>(fLuI~v-C{lsk zB@NBfVtYoR$#>WeLv722l^~Df`R6s2Qj#v10Fh zIY5@0Fg~^KrKl}52Slu8$$0G`J zuF#P$T!R<+-%H;jPQEz1Q?VLYZO{zi*_5$w>Zu0F>F0G$_h%YqoGCN&Mtsz;C=XDU;KnPuzVs{{>P%wh!l(U%+DOn*_n*89lHc{( zu(A9chVMbEHRVo5tO-KHtklF=+z1w`J$Q{MEJdtGt)3;+;2(2sB;57Twf*n*{8bWwa5o8eY0z>uQ;!b+=^7xU7a@o4T0IqpB+` zvxmr_>BiwgJ!l192CcyV2uQy;D{yg;Iv!#7@uEV=`A(n{`-U-3f)eYe)tu(PGTzgkuQ_{}I@fwoGeNu!;A{uV%} z>1(C(QipAzEiQ+0hUqEyhg>$w6&pZFC1}CEtKaka)sYHhK4oy34|MWo6_42NWr>~I zawS#f0JE?p&Msy2TY%_1h+Ww4&NIh(m*+qTSsvW(9fXdkpJ8yeTUqA?P3SW*g024eL^1M4GhRU(-QkLW7%_DA%C95E;DQ+O4;sXXp>~4GLz289=+lwiEgbDi zShbk-lTf6~>@=)m#4k_x%a?$XI&q2b5$*<0WR>{#u6@g(C2S-NYL$U9DAY`J5v>2O zZlZHQ_>j2@RHOxkEU=($j(L`}yu+b*o#1RI{IR#H&;iF0rG^&4ME-uEvhcWDjmb*R zzmAtcBns3jpzpgM?$G_0NWEhVDCBO#kB5tXlskdLv z3h;NOqf{72OjVw*#b=B6;x z(N>4zj2mYhoX!VlUu7Om0DT|v`b&pFZ2zu=QPuJQ^fBK9qK^U6`=DgyX z7-<(JZ2C{tb9S%W0Y=31w~}$5Q80tHSZAHlXip7WxC{7jR?->_RO<>i?87u`YiY{^VBuA{v>G? z(A{%6*+i;t37pPq(i(DSYQnk&!B96+VKTc+Ve@ZHcJpsccAK$p`8ZPir=KOCru=Ik zY+9eZ5`L76k~nH!XR!)T*&sjT4turgFY(THoAfdtcNUdd=Zwy~@E=a(dJs4f2<-Qh zP3@m>Ht%wNLce~QGvO3#p5x=pT?3_)P*K{!GU+rL^fo}j5=5|nZ4ZOGVIZ52%vFZ} zI<7R?#ysw@qk#t^g<7Jrl5OfK<X#^#4K!7~C7Jsq5H0HRqrk>yMU64=EG(*#<|UqaSPJrriax zdC5cv{Om|gPRVkK!#R>qJTYeouatNeYDkEXn|d$JOzfq~i0mpPX*Wg7a4W-V88VJn zN7^0>8}dD(xaRBf4Yuw8u$*+?e~fvy6AC^cdx~VZ0x=@3ZCtV8R^AV`Msu<9^G)^A z|HV`_Ui^!AfnWmaU#40hvHc*p<#+d;-dpJhXX}-8zXJ8ZK7pHx4NJSk;2#%x5)9IS zjH$G{2|6&=R4@aXs+1e{V*TLp4p~T|DpO!jS3&~S1X&|BQsEvj)VK7hRbUeE2OKk| zSsxOa@@Z>|oM8{K3T&-MtGu{7FWJ;%ShMooXPsbe9bh{$U`5E2_`?jzJQjI(W6-;K zyy$*@-2cv8kR`13UATZ&22PQ^4I0{8hB(L#{c6I)DSPwuiVH%5KOw@8`V#{(JEXznzyV;$ zTalrn-9-*S^#f@IhYIMDXWTL$GpRlnsp@O+)pGHntY4@gJkI~)>u{M2w10sBd8l-B-w%NuY|`zlOvC_H6~8di)_}9jRZvYbKg}oV z*bwjBHtr<;2LIPd>Ho~u_001&*MykoVA%b9Z6V_K?)vefTXmXXkAIxp$~eCK8mS+U z6*mFe0YFRzYN;da04P!sjli$Ph=9y?cPjV65$GsRF1Xvni6@zN>++AgO(q2m z5V?yAczXAVeZP2m6yJa2>DqiY7YF>Z|D!Al&WHYwlv@RiM2I83!yCQj9Nh?O7;&$;?|)ioaANkhlJ|S_dy` zJC(c3TQIzGHksY$z*rqq3>p83<(3ZlpSk$&j>cb`6d}O%9vqQtOtZGiIT}*kepBBx z{}KNlED&LQTXrU%WxCYshT+}7!hviY50nH*&H|(fIyDMB>Cn5qycZ*Xmy1BXArSgu z1fW|se1eXeh_)*!b3J{}3`P865P>$?uoU4_+5xn}$?&D!i?fkt9j>%uI-ql}$+Y&dAITZ+m3? z&g-W6%+L4u{r7YnPrBXLd7a}m&KtA%%wK2is6L8y-tUct`9;T#31^R+Va5WFbx6bt zek)69iIv9_5vClzo|GA08Q*Jl95AnC*a1c?Okn+Qnn_~7f2A@R8JxBzpKE*7cXrqSW|X@IjrU)0 z+V5h~5h$;o{UE3#T#i~khu17(qDqUco}d3?Lp9N#aXp-@z;J?Zn`LDurQ>Trv1bLQ z2w*fsCV9obExXtxkH|y|Q+b4KbY6JIV(9HW^QOeKhrCJ!M5C{xNjYS6uGQ{bq-(bx zc#0G+^KEzAK%RgkA4u^6p8vnbxvV$f>mErx&(7<9Rp!&F@=_3&WsAlrP~>CoB9g?r z0yTM&h4%3v`HYbV(zYHWZIR05#MFT=ip!!P+qfF0C=tC*9$4Nr1mU{fO^Qy>#NZLf zFPHR%0t$C-ZM|z52dpVDnXLiz@^;;lg@Br%@ds<215wJA-T05knnjJi3DUmxf8W3_ z$wfn8BkVn)=>HS+^?lMTFf_%2^11WPs}`TzJ7VtlI+-)stc!;}(Z3TTx1-~^dx5aJ z%O6;*b3UM_Q}p}|;Y1)yl-4$QeBrRCgmnu3eHUuKWL4vR$E2e}i>DHIAZItG{K9Td zqO{FRu+qofB;dk<%(mk(LgWeN-2sE&I29M`?wv^ZCz)IhT1t%RsfD8eZyNT(gW-kK z#n4~?mK?_Hf-eK_<}PVJV|bF(7s!4 zLe#*&rIuqxDxl!Pv?yLU((Kj^3+3g9pwGYqL5b91U;%;@j-nCaUi}AFbaJb5d(54H zAeditN&E$yk}(g>?=HK)r8z;JhnZij~c7$CP?<+QXz;|2yQh%E*f$Mqb1{JJhX5HvWzHfEZ3j8&70Tgts_9xSNP^ z1Tr^ak5@{1ZMVm^7PIeH63f%Q73(Wn;qBJbR$#~UlRzX1& z5a^8|6%PxO|Fs|E3lbtBQ%n3gGq-uI_=ua}XBhnN)r8Vm4xe=8<<`z^DN~gZqa9+Y zM!%j`hW$0R`10|!H&n@45>JK}s9Zc+9}Z7@o+dX+h=LGxI+|g>Ms-2I$=I?9stGGR zt*aRdL8GJElXdfu;tn^%n~}T?t#^d;KhNtwdW7G+*3aD2Z*76f*R- zDpQl;B<3X+?#?PeO9l_?Bix!kFMuQHcJ31nq0DELXWikF)(Au~(Sr;k7qQef*Y553 zPb@VCM1P|NR-L8)a!FEqFR(o;ANS|0zk#&cH>1p-xrSqUd-Z01=~tG@k;+vahICYX zYa1de>q|P_oR1B^ADhfQX^anZDJDbBtJVuLEQj1>60Py?#0GR@sJK6z;C62FVbOP& zy+EmQr4bVWj9PxEM;^v>srBo?*Mhac$k)R6rKx!Nha@k=}-yo|;LKZgNDhY~mg_qw*eg5tY zNfy|PD5_-6bH5;!@;Y+6HuEI0@r#?MNi-j0I}zdypLcX_T#{3WDE8+kq+!$_SiP{0 z3I>Xj9pyJM?NQeBZe;wXo9ysth)b8Ud8vBl?O)7%$dw32G@{I%kc=iyryM|SjY*=< zI;X~@HfSkcr_97?1?_D6Z5&r(qB>*fJu%jQpZeoT62^)Jw^>E7W=)XO3YH%r&Wx#v zA?I>ogDnboL4UVG3i7L00-2x`HE(S1eD6 zgX~?vizujY1uD!WzuC-RY=LewTjJiFpEoYJZ`jCXr-R1t>^Da#(b~bExNYzqbJQ?s z=g76P&VOgEQoGA{_cBwQQZyW+998H zhTe-;9Z-9qc(_z>M4Alb*sN91w|ee}qkg+1(+wigzokxjI5Hd7o+-GrO?nNwY{>d+ zp!M%YH~p5(&x4;$)USQ5924ESP1kwq32~akn%`6dsB;XPhgX+SS9AiK)zHnyHR*?5 zPm-;^_M9{&=t$?6wiK~?HIfX!BT3hS%e3_b-*Mu_3Kmb5=O<|qE)ug~uxispJXCMv-xrLO^u_Shr>(uW2`Yu4vIVvx*mVVm+NNbD@CCRAt zCf&17x*xhmgnI`9qpoO4LPe;XK9J@{*LPQlfZz->M6Zm_m`JY(hDB4FYkXv>H^i** za>wI3zN=XJL$P@HEXeq06-(A&%NIG3E$O93A`+O}*(fogl2o!ZRJFTJ)E>;dHP<$BdGrvRRIA`%$gqP6c>@^rP{*wn ze0M#cWENfvECYek3e9OzYWghRXe>WSo3-KsI~Rq&#fj%X&Gn{X3(V8J9St%JQgEXA&*Vq>$xBa2 zX|5;61kH!mq17b_diWov>wnO~{6@}L zpVrw6Y$TU;^cs@ZQO}QqnT9ASKWr$J{iepBj#yjZH5R4SF6(bUn7Iy3Z zsD&lsyt;=`OM~`r_>1pj@U1fvN1D01E$d++i(hWZcFkxB5MYF#3T@)AY>`Wvh#G9O z?x`7g0pRrJ^ScrA9JzX+5c_I!<=y_chyOLA6p4089?y?x)Jxu zT4);f| z3~4Y&3D-5J0Bc*On1G>KKemMgT!dvM#-~T?uMP8v-GMDT+^>9y5wIAq%TPGMe>x?m zvVN`noF(R?`+arTca<2~yiR9Lr>KFqL}>fDqS`2X*i=CL_QlyRXBwX7zNa>Us<>uC zN+CKSPtAc0w~jll4gU-9w|ghFKMyU^ccqbuS)@CzUBP^R5x#~{bMG$j}jQke|zDhzb?iX=Dbwm;TCr$Vyllul2Sr3Z0a|G>?)DSyOUtPnO0 ztAx$^KQR%h*!e8|cQrV+KPv2}sS8O=%eA=k11>l>r{8J}4XFgn(tt?mfM-}b^|s?A zH>b8aX#1Td36}{4&sEf2LA-Fh{jK|yzpfHm9d$s4acH32|E!3!;M3JHF50_%v(_>D zA1iqN4qIYbXOCfI`of3)ge^73Ni0>2d)Vr2pVJ4F%Zupz0ViE8Bd=Uv>VCjCpLMte z%Ny%3o>>E8A}!-jt8%S1yi9w6{J7w{9a&yVC(b@!k=fy_oHXCtb>qiJ<|Y@8Z$x76 zdi|(-%l`>?(7uNY&~=E1V>5spvCV+$;7iz0hdKj?oLVjGHOAfhMTP4rvv&&@z-s6{ zhMU&)fG)mGtw92@+}{*mpjCjhi-TFOW-sTHB0YUdxwYkFLCJ=*+=W$?EVFNhfd2zDQZX*-zQ1X|{qi05X1TiJ{TcyfV7)C+qa8Jb2wd z>1vKLFI8XJZV9%}+pNUGU(KW0Z-tQ-Jwdm?X_P+z_0kZ#N1p8H;xS)6csX zr1h-ShlN_q!x#@eBQ5enLbjqi@Se);Z@ov`b)S3wNKdXzjXJ^i_ahA|Qh1??&b=bH z!&&_o;Rc4+V{^==*ps7{EG^qmi;ituKx=@`ads-s)+*81ga1AmB@a%FnunUHeuDbTy1%G{y)F z#t!fM#=a;CVpNA`1yXJY7l5@s(5?f`?LQIwzu*V)`=()!^HzaU2%cSu2Da#58B2AEpu$n@|z}&&_EJF7aOvYV1w*0stI63OEUDq1Vv8O4!?0+GfTkOa4U%ya&yB zBXv)xw_PCNgpP))_3=CM^CTOwjlP&xeU80uR7D~Ay8!p7g4W+C8+e_O!TE_Htif1d z*!iW$enr6>Pn%$darDk!Da$)63+$C{tMmR9^ZON+2kFzDi?xtqfVoP7q#T z40v#V0M%^Gl_nV(a7Hwd%D*HMX$jk*OwuhIIst*--AWs%!#LjZl~uqAmzGQ zg~^P^e_8BIRqBwFWc_@UOZm+XAVv2cQFA-+CYH%7H;wvmeUy;Yy#R{0{6s^K88O9v zUn~;fzD)flz!kRm^lSGF9I8O@Rg#zJ6s?~iHfI6b=ZIN>&kd|_BtNy^zu}vZZf0)I z|FHper9FG)z>dyF9U5E6Hm~A|t{{IvZ_}4sa@}c4Z0~d|Zu;#9r$O!>!-zH7&kl*! z^t}MIcm2c9rM41wnQmuSj2t;?MH)0Se z&%wXoF-(Hvyh%0_!|zvx@7ZPtZ5umNRU^fx=j z&zJ^Agdma#p;o`Fz(yN^2B(x)jStX~wNT%|Hg9roLVCSZw{D=#o7jA6V{p4E{LjmvP%of6G znFT3|rB#>cSDp+k7}(nYpXqnfCP=#dsQ&fHRfVXb;emDL#17K`ff$5+FnU#z42@c# zVhxz>i9vhrlm6JAnn?hcn)LjgguP4+T$_)c-Xc05itsDFBmcPKhYu3 zxopR=fDQ$zo@?@~FdKc{>+RsQL7V1*KeBc?iX;Gh*ZntQ9bp}vlT%9=CZ>6I4{|5) zN4%wXwqa(uv!`FWI0yoVWHw}k*(3kzt*SI7px4l{>8YoVz4Md+i3I&6LmooaQ1AI3IMB+s`wX%%eQFnRh zR%GkwDcoqk``!OE@7=3uKrXHD(T*&vRr_nj5+K@8VGCAuvWSKIzI;(GnwwYDp?q8` zCn>NO`?!-FiDPWQ8CZV#$z&rUr^~r*dD%0 zps$RaKT!%8h?Wu)-Taw-BfO$$MYJ$!9OMXl6>7G?e-?S($+-4txYo_-)SAQRrGYZay^ z^NxMwQkW#xE>R9gOHp8drCMC3qBIbcABRadX)hg0@P|)frM{MFUD#ZzGt5$?^KQzi zqo#-apQy3_gVhkTzWK$uRH*x<*(lMa1yYAuoZm>Ea3T%^V{;W-st&U!_p6fxmhzpeGRU9 zyF&TNo`$XE6pbZ1bl_MYU%b?oyxpR2JZ+zU3OQ0dJN@-d^_vum58WE2c)usKF!tDIl@mwBM#TEQ1e*m-xr;5?Et^%|s%X-WPp=3XihVIv zd7#+$(!z4HUSrcOqffE(*XbV&T_79CifamHvvZj@h>~4;IZw;$C}>u0A^ix6YDb0W zLbZ8@oO|lRV=RJ>1m*UU-?VUUu|D!Cp5{5t;TD?1vlRr--g668I}^$@*JfJ^U+fOa zJq4=go5UmJfYf$dHE{AK1U0m#4ft&e&rATksJoLs7nPH7(C)=j0U=@L>x$s|PLAv?wc~dPj46TZq$kYJn4U5 zec$Q>apScdErEq%Vb*ykqx*gu2~kOx8eA6wBHCf=hP7r>JC8C<{H&{9Og zfhF}T3o`Y4=zCy|^Btf3lyEA6se*%UY2LR;Zjls+9O?bkKf6x#1bS;tfkfi`#nS{g zdFRy4Cf9fX<#=cjL)}W=?@F4m>aRxdzyn842r4L_n?b+5KR5juHda_EYX!pWTC>!^o&kB9Sh>bBj0^ zbKSCD)58>=A<{w8M}qcc8C7GOV!sI63)?4&5l=l6N_k*>wQKjp?XWZtW*$cmT&MKe znUDvKgLJ(wRR2h0B|M7WDNQx zwbpNmy?z&8Pt&7)DES3#sw181V_c6$a-yoebd!Djt43mzbyWrWVorE>xv?`AW?Ie* zHvOji=2jQRhy;tj&mVO!Yfr z;dH0omVAa`vh9F>U-A|{e1cfN%Z9%@B#IiVbL;}A3&R&E)>&mKW(H4v&=-4v51C41 zih6WyBHDs|r(WQZgo=o{PFW7x)IxjRH}-8P&I%4vzQGPQ+V9BH`9A(=HA8f;et~zs zK07BoJTFISvD;uYcVL*@w-Ya8B3)G=vbJ=V^JU|E(y14$AHMAkxMz=!Y@cLO*{pG0 z(Hg0mJ@oJz?DYif=_91~dDd&*m6-#<>^&q&6E9*eUA;Hqtmt9rQeINEm@{$}D0nYN zELAJh|W`UClHmAs8XOfBf|~F^PL@?hHEX?oi@J|9T3aeVItzZP`I~bP4p#NCqX%43uov z)J|`N2cb;k=Ush}5z@)`gNHmzAaW}iP~;*JkaOInAr)0#Ov)(B$aj~?@4cpO7+D&V z>f^T3QywbhJU#28e3wXCi6;r;VK)iP$|2)Gxd8ga7eGv9=SWrR3<&`i*I-dJPky=0 zPu^Hj;rU9cQvX`Dvt=qfMoGOlUfQ%}j{V^zMf-mJLJ_y%H!--ZZ3I@a{d-QrM-an+ zik*{dQHQo#Cvt;H$p&=|Cq@%KJRy|75Iap{VSR6ug;1D}`!_w#i4|aamS(t2(b5q= z$)E95+);2V4J?iQ5VB)?+OTs^!TbfGLoXgQqQf$Rm46=cdW!Yf3$h~_aqj&nU?X{t zosDZ^Yed>GImMo={LT~{8?F=Ybt}uO+V=WGHorN`IXWqP;3>4U13Sk`{A|SC%PaY3 zMuW7{fQ0!d)u^w_0-e$L_?a<^#@M)4hdRFnmO+($A9%T|v*swX;h6kSt{jt?efj9I zmfNv?_2`+J$oER{7eA^Ol;9`n@Jro|G9HV~1O13{>el?J_XCHvypmm_o`N`le+p@? z?DKLNk5KVKQoc=)aI*~;So?l98W>|Z#NZ}U++!v`Syf|nTD{~OkyCPZn;oQj>{;5U zX;TV9lCTgIjeMwl$3@@VBC7I(L{pTC8%Tx--lQ*gZX5ZQ zFX3VQuJ85CIV*NU$rg==`%LLZ$Telh}Z!T9T?wc0U_R|~P*BTS4> zZ5>RXN<{C))q+Ht*}sohe8>u}C}+}&-xWWe zKBQwOVXbakZfsy7d@B}?AXHvQAKphve9$?n7jSi+)A$}@_BYRWzG5BjWtl--xshsi z>(t>|i>AmfkDUDY`wb5olW>+wYoBgk#)1UZLn!m8o=(R(D{!1F`hH+#`v_Ym36K2V zYj^JB<)uq7$@=g??qpDj#Ov&p0JT(~!a>sCUj>U0P5&squ8b-`wJZ^{|E1ztDb9(M zHNo3y2;76FQDfuY9Zw|oE9f9kVL|ciCBHFVrG$e@wv)%d-4g7iE(p*1JeS^wQt&^- z=AQn0y0d29kXqYu5SqnjvCupUyASW*n^p0|&cgs@voDP8S8rK-CH8SzYv2kuQapxQ zYy3~~?X9$7Ec z@33QrB4;Yg*n`~?kHtT>-ZD{_;EI)2rbkhEPwq||`MQZXDJ(>rNE@J`SgIG)fa_Xl z=p3C`+kYyyqD*wJ32UgdYC`m;Ihi{6$*_pQ7rdg>$!T34KKX)y&n&iQb>a2Cw^9Lq z%N_U+BR!MjK<;7PNR;GK@MPyl!iBlerOQoLTy(Jz0NXB;Ol~@SU;9uD{2}cms|jJj zc%iNE*@Bb~Kk1a5YA^RG(-uD3loMIPKI6@2#^88WkLwo>olD4MTl1koqEMz&v)a7> z(*(4XpAR?|lU+$`gV#oar3Wdh;Oa|4SgfwrIh0q!m<(bpp8oW6B$c0-Yk}#m;6Q}J z*m^46#l(kA^;*sUFg(+ykbccH-alF*GoQ+JB2WY*_}G!I0}`tQ+_aaQz0f(d6>GIqb8 z66+Q%Kb`LtoP8m2uIPNoed!IEO5%+2-bD1~+TLANJU+9s%yd(c>^qvXOcC(1(!#4Uh)gvT@70_ZuM@R zV&}c|i7Sr7c>Gw5=#O03m7Xkt$az$J&7~@;FFl zSyt_5iH8WPWIUAAAK~(PTAx9^EEfxCv7wmmwu8It)@wYmSUW&w!T9>g#-7Lc z9!Ju%m=fZoo1Rx!B%$}=L-WrTc%29}zpKPAo#`TGZk)NSf|?fgs2qg%STAg|7=C96 zL*n4`@CEy=OMyM}7tXmw@eD$DAQeQEmGQ_w+#gj!t;;kfP<6Ijw4Y_?nn;OE8&;%C zVaXEan;_7pPN}J@iRq5YA#IhmD-tt@HBN#IzQ;)rDs`q@ zlayZm)P%B@Z5F5H`izu9#t$}>qoe|#xKl6>I7pu6G$FkYi zG;|Jc)Dqg?YHz6I&R4<;$Gen!24f(pn8#IhSF?G&S_TpCOU;-0qG0BELbbmu@-*53 zGqouIb`(N&&bp5%=j}i_tLXjmOMUMM8W->FU8q*-^trH{r*FIFjYb`WMIAXUKQX?K zgH6Tl43_)lI{GCiz7$Hk>9zT18h^N>`=**mYX={k5{wWLr{PAap9X&xocD<(Hf)}J zjLnm9u?fPLS+;-JHT7Ww`SVc3!FkNztbCB!?HAlARo1Wa^7a9`EFWwcHpt-6Z-|R&5XJ z+_Y}eTovc9Y!2b?W{5A-q_g1Xaioh-OJ_^qMi12b;A(swi3R@KE&xy-d(rD2L4^{A zImSibeArJ7Loo#N1nJr}5X_bRuL41^g%wfQXF)xSX83z4&(->hl4yL;ji6~>&j}il zaSBMlnVqld;!$aQ4R!L7E?Mj~6jwZLcMW%s(FG-irab&Cb1tiAEXT*6HN3kR)z0wt-wZWBvXN zR%9Ox@WR(MsF)>+t};L0%vx*5Az@O}Ez6PXy3LeM-dgjnbn|;+BeE-$Oau0jX={zW zp^sOCYOt~b?AL~JWO}NzpCBz=uk9pP`%)KGA5wS`PBbSq_n^T{>nzP-i%vn?(Jn>X zJ^QaSwpX3tnh=Oe8{VFKu=(V=)Orr+AS#`?V6OSKvkZ}fv1sD*f-=4%0zTW6aIf(r z-m88CrsPyEei*6-{ZkKsH~Oz03e-5mci#+KlNr*UGJo7*#Yki>GH=p)ejWXwbI;UK zwFr1Ap_AlQ89D<>3O=L5?WV(RYnaAMf@*_mQs=kWFg&zc;_hx%o4F?6abYIAafav0 zgk9zwL(NCc-roVlk;lbp)B?7kFU}#B{A(t4>{=^!x zw9jN*+y$G`;NmO#U&K%W!JtDjg@)u5oF@}*Se9QN50L_a+8$D)*`eTqrtx|BI!Gu? zvkXispI*@P^V@3*zeIic zdRAmHcEnXzX|ux;RKTr#H{#BGaa0%{yB4hzvVJJxy?y!2)cX^2 z)iiV?{Q9kH?HeUX8sfhEU);6FAcLB$v_2{5>C5$_^(rz$Sii;k}JoJ zdgs(}iYiTdX4{g+Ekj|P^z@R&RM}nQ%ZB2W*6QAkWH{+Dy4vjPyVqJ5yvwCAOxti} z>TX7{9F}idv_&dE%cevs_?b^B5Y808oYH=dyU{!s&g*Bl+m0Ro*>FQiz4fOeCTe<% zE6j(djM>VmMVo24+7!A%YBtEL8nKHG@uqR^R(s;yAS0W$9Zfjsk@KOR z2ZP43j@wlsGefN?hc>I6CtZI0){e>@%Dio36?0_ngeVyc$MtzdCz-`ro~5yMT-m9YUfZHQNvGEKwJn3$x3YIeGGuJdJS#U+iFn9l zJ&L;+KGx@Is05w(IqJ~37EOqv^S626lV6OQV+AoDMCtx|snN9hZA$!e7vFY#;BnS* z7%P|R(()j?K)F3Gq|5?$4=X~@)=9R#fMShW$OOPw5u}NtgAX#N91h2UhI+4n$HJaD z$Fn{hpNnM4US~A>Y1qzg%p+v+0}&@nZN&|nUb}qj1S@Xa{R=mRtkoY?rrH#Z-SV(B z?09w$!9peiCyL;&Uw&X7oHG!2j>2b#ltHuo@`thzGa2qVnHrg>D^b+^tz@iMRZfp~ z@?^62vvA_9Ub(*`;~8G#xpb}4yQ609@3AO0RLI))iL+b#1@1=YRG&cVy#MtSzAU_S zE~}BzDPFxI5$(HtP8yj7=h4}!mhnt`q~%81=+sFV#!`c5(NKoDqR8*FCr2LL^0BEW zeDJe8SAv5upX6|xGPr4-{365YB!pB**yG-yND)-km+6!{%6qQSfAgK*K2tH-h;kcJ zcrQyFY8mTW=Ppk7e5G0(iMDE?U$eFN)|CViyZI8Ttec~)UyphEJS&;?zshr+(f5mC z8U}>of>&F`{|>1AK4`Q&!3Ft^@6Kcwv52lthLMgZ^of+hO(aky z6&$9;v3aGXp-aP5^KL#x4XWS>Z4tZ@)&wYG=Jvvq_{+PO>$*wM_L#pO@-PriVk$YX zynT^ZVlq!!LVvn<&0XFHag;7OVzN3#aM+;hp&Y38SczolU*mKOQ_+? zuj*%#o3Sokwc+gt7(RSt$J(Z*3gmoV_-F;z$2#hLrEjg>A7$a!S1BALmZgJ!#=~b@ zM@{J4XeXc0C1ib+?$YR9%arKgbiqVdRcxTMpjS%|lB7(OMR8JB@bj%aM=j(p4F$;h z&6yd+s3#yzNvD|&klwaN$`LoG(PJG9E!V?dpF(9(s~KP9Sf zg*3U#@Ki#DtmT$RucstDvL&bM(%qKHrw*G%QdO;MUwlSoQ*tOtFhp9P{;25{yZp@5 zjIZIH%jY*Sdw16s(abPkvu5N^AnPKTU^(I_=zz=AF>+l6=~Ckhle=de5@ z@3eL`Mk*3uw{quQ`gyuIWZ7|OaSk@{r^emh^Rh=s$ySEpIJ2y>IMXIs2)s9I8w(EN zt5LeRWSe;^neevXQD*!yhQXbU7eFs2#4GvbK^W>-lrzn3mRfJZITxPG(y&Oe%M+u# z^uWh*DbBOGcEi?DYw`-;7n*jRj554U1vV$MEIO0vZIz}CrU^OIgxKu!$+x#~ed!ja z;clOJ!fMylR?#MM%uM7eTXutqZu45eDNRnk#iIR;(m@3JxdIk;_dC?i^Q}YvsViQA zj~u&F((;SyJX28wt&%hmk}%P|M-c^HqJNn~gI`+-Wr@N9HWKv82Oj63f?@lY^VyG9 zxcv%6s-^@!&HA$nv*p0~t9=jY^~PDttgNggw%WeUoKM`ze9`)RSMv6dStm6lZP-dx zss)`beTap5!b5af9GAm-1XA%)Da+l1!(+g(L>=PO?<^FV;Z1v%z>UR7!?E>MV^ge? z$zRt4T{D-UyW!5C0tJT!%8mKdH%BM)_yvOSQq3xCj4&tGRua>g2);|%b?7rAkQlD`!3p=pGpwn|=yW(ma$SkHrjP~{Ls@z2^ z4$k-CvXr5y@m{eT9f$(nulqh6n%%rE@XE#$I6t9hN&(wq3xCgfx( z+%|_bD<00YvS>{*ljh#I*n8Tx2>LSFl3;XISm-O;83ItEFt2Y(`dZa3tKS=!S1u|c z{h;xh=)JGSejvKVBH;7evd=59>!**^6suh_La}6>uK1178)jxSo`n0IV8$ClFtE+b z7#wc;5t)7TtyPWQ`3%Q+ar(PR`)B93%r`>=FnPHz)tj3rzemhb$+q`od>Z%jMDBr} zvXz+Py}UV&KkM@it0zr{@9ng*rUoLY!f4$IisFP1r6V)P^i5S8Urq6u1udtuo=?q4 znsM%&7=Mv4J3qlgwJzm}Rn6LowA?2z>es@gVsJu6wd2ZUN2I)eC~gXF_f|Tn7jb`N zedqyhN;^zEz@--!0n55Y)uW;=v}ZMxRQ74y!I<+v+H>E>N(3J8o-X zyYIf|fP@$G;Q`ZqD?`T5%=n%C->}t(%kSrVI=?7~EPZi|WhTThh~Im!_vWxN3Y(_I z_5vBs1v>4c!|h|q`|KtpTkm@WAJUTKZFQj1Ph&dGD?T=Y^IHfrPu{ZgD6gM?@cZ^s zHmx=q^nSbIaNME++9nDGz6AMLV_-x}(}U?Ry=Z%a%Y?7H95K188foF2f%3a+qpc__ z*4e)gNr7SZ8Fcu7a6>N$SS3QWABca!&@yB>8BKf$-QNgq&j1TO&dw;UUN&ICFRpD1 z!6g9D2_O1>-R&y8nHGIACPj*nNj4<7;?jK2|NiqMsq{+yu)?-~A&1B59ZMfN+lzyG5@7z_xsoI|2$dNb4)Rovn`6~eHvuG-n8>(W>Hz=<`U3z^rhn~<;buHL}gHb=d)(p@lSD~>+P zY-@DZ*9dM2o(K<44)Gh@NdWXu%8lWMyhtG++_fRlPu4 zL88owK;PcLD?(8?7({NMCL?3pfMtk&zaxWy{ec5Y(6`{MyFO(dT>96TcB*ov{~GHb z90D&~;rD>E7%n!WrcZlA^>E>urdJ0w#cx#4%l-mbg7^*aEbWJOd?eY%CXoW(;*53U z$VU4T5BZql-ssYdvE}ndxa`<|nBX#>Ptk%>-9x;m^9Nbi=k>q`@LBVZs~~dp4-lDh z4z{;#LEE3d`JyM z5pDxI+%YM)4#X;CuBeQa@dWPrm@;+aWxy$XdxylEw%*AFdnMag&qHY>4kG)jYlFG- zA}Mt=>$+vfPk8}znD`V7y;cn?IEFS*0(W69ab3uI*rBAF*M;B-o3#>oDe*pLpJZ$L z)-E5N+!Z+cafz&NA$PBYvHuS6DCth-yP9W{7Eg4`y>M(i!K?Lz6(R455!aK6p}81y zF`xM`>)KW?WrFnxK3g!TMMy$Pk%ImR8!5U#y1WTngQr%eE`AB@H$vec6n*YdjKuh+ zEzBl8HY0XX%UZp&+xFQ6*%f*+Wr(ydc2~Wx)~=DWcA6gFU--<_qLoVuf6FN2PVg_} z{FMd4>)pgFzZTL+a&6P#rfWjoNCNu-Ox2m=3~2?8izp>F%%#Rjh-9JsF$1>j-k~4pCSnu+Ixsd&%Ww&h)XYx0zNn6vsNpQGdr5|O9 zV~S$>Og?$zO7{u$5;pTJ>1%atXh2)W@*Ty$kl)r2YvPX#)E~PdXM-2bd*UZmBf}FM z(uj2=GVn?o@8{)~m$ko7+E9QfiS;`g)<^QAa}n3B8Ca z&LevBk^CQo;|wEr3#~?|d9LKZSFp^lt^@k}{oc8(HVxRG*%Io>4blGPGdiBYRxa+*)c2<_YU4e@3W-yN$(svXdLZS-kD#UzY{A7;cjl$%K%By>9*s}V% zr@{gKIy1#!cI&-Flz0zi;J8&Kg=i9aCg5mL$Pw83tNIEdsQ8J+&-EvlV%a*v=a_6Wa81+uAVtfu{4;5b#0%?Vgfg~ z`;QiR|rx+55aOM))QTR4% zYjQus!+E_&bq`wZQnZBOf;fo-GZ16L7<8n=MWC&KY~iZk0;@#?Jr@+_zyoy!1CkG& zZ9DgDT_$SKd!D!_L{{JY6Com&*KahNmp?J~SZac_V+A}WopzUFcvv)40dfR2^Se*4P(gHz02_fHfL(R&tTaYUpK4Lv!@_P~naiA;8E|`+Y zX+=qdgp3LI=t~qg#i-#<9w9^9?H<;#1|O<8Y@hStI0!FbN}0`I^-U z<$EFP=!9sO{vF)9p-Q%Fie|ODs6l&AJ~=2`^XvOq$@V^BsZA_u6FXeQ_VN(Q#7zd< z75!OYGz20o%+0)lH|>>%CWSEtzT|C#0};Oj@Z`G>F20Z+7g`kbN^2oaR&m zRSCMRu4IPs4FV6uNw?Ysh&c+1`q?Sq$K)&ab^})bXs#uyC7G~e`Js>byT|yk@f-Z# zMXh3>_#xWYY@wjlQ6mmW@9O9@#Q%BompAbFs8)L3%J#v$WCm02i4D4d8wPaa8(hsZ zCs+>m_1VNY3X)}A_WA)6{n=62JjN}ms7XvWvG>JvneO$>7_k_; z#B{T;N3E4QU;Fl?d~M3+ken=0S<|beAka^FR-FF%r<7t<7oDkWK@%xCjX^2O-E02^ z8^wAU0Bqbtoz2URFHs00h6crz(XqkR!1D2)Mc;v0>HV0*U3R87UW>th4nyDC`q#ht zGHrM*7DADdzjNIyOmI`)^0xUSHpN}MxcM}b1u42OtctRL%J^uT&TYsKrvxmHF66{l z(XNRoJtlM5bt@P!#*D$69^cq!ai~k^#a?hXZ-z(JkYKStz_x>yf^nJtqX zG;^R6MskvsMh=jG3@hCQnd+7=Ph4{ys-3lo7kn3; za|lBQ4(tgTq7`BDUt`tInW!z0hBa8~#>Pxqb15z;aOGI@~<1km2A=$)dv?aS~^?P4;?b?*XBg5)QMp1??Vt z?D$Fw65FpkH|(LA+vCaJkamF?>wyYmS;ed~H*YCj?QiG0YjF9xT8Xcw7LbaYy3YMW9V=naWzmJ{LY0U%z= z|3c8vquArIrzxAq&G*#%Wl znOh|(6sczQSW&1X5pZ*p#T!Xx2##g1?E z=I{7r!Z-Yq7ZhyerZBgPprLl65Bhy{_abLYs8Lzp^u4SL_M4k;Z4VK?b=|_i>|o;( zXUKw~h={8ozkT;al8-a{rsN^`<}4}FuX~em z&^4Rc?{`R&zO2h)_ZWGKf>PSY4URgIfch+}F;#FU6Z{waa3>1Ps|7!LLyGitDI;Y) z=@XwrSX(bM+G)GfuCq(1@V6mcbKXRtVsFS<@|UypFMI+q@Ct#IfFqd==xKy8`B7)> z-)Pi3_e$zIB*{tzlg`MI{Gl*kR&Et9K|!En(hwk+DqPse_s+Gar+&WSWXC1_p== z=W2@!TH;QqcW$|8{Az}3c$&8HJd@HEoAVYuG|ST?BNfI17wYr_H54rvS_;le$BRQ` z9q#6zm!0nKTsifhhpX?R91NcP8y*R%j*%+SE?hj^jR~_>gflCnE$IJBPygukljk72 zKAHw0^?s2`_$Hl4F>MF+KS`)p$?eiR3m65HpCL^b8peHaq{@(Jn|ad0Bwun8Z17uOvVDc{tT-Bxr|Re*^nTSnm)3X%qNq{BYzvw17sm|e>Xg@nG$w%*kgqO*hz)TP$TG6(Ecw!*4Hp5VXa7F{ zp4Z8C3X#@8qC`rLu5c(T^q0fEe-oPL9JdjmDR1w_Za6sINq!r>eACnm z@1j47pVE0pAJ<9!=yf=gwI*nx#5{7`wl?M)Ua7l5CR5?CiF#x+-oiuT^Z;M0qu#`Z zO_cPHf>SSjI+bJJH(>9Qb13pq8k2W5v^{^1PIe}}Zctg3IU-sEgKaXhZBIT*h&CKz zp|`S3CdB0+7OXIQjy;epN;UH#&a_;p{DuqyeX>dQHYR_^#^nLKjyGLHrGBhnf7+8| z*42*e#OU=shwtJJ9)johQA*2XRll~%g9`lBPacPY=ajKZ>&!dA+r$T6`}u9Ejj!AN z6Qt!cO=Y)}a4(@bt3CB?oy(mPRhbgIVos*zTuz?!&yu_PBg=C{-mZ16e3AbAgwE^6 zHs#r;;GLKkG`utbA2;rye;|#cn2Dr(@ta^ATrUDqZ5BxGARvLhF0loJlN%K<@B{Iq zb#X5HVK)674=@33<4N$$ISzT$>hHd|I#G`%_nXr7x~yO0V~tU1=nLZJA2H*`sv)ct zE$$UIbDe+c^UCPYB7fA}Av^&Gg!-s%07G|+Y2g~e85b1@CwVbsyepX= zN&&#vjpcQ_mQAM`qN&6xo-Q;zZRo(Oui^s56I21YzP`^>Wt>~ zk3xN9;Uk%&y0k_~jW@{jClS>aR%zm-)6!6yemRFDvXXM?UcY46Q+H>8+hZR!aF|?M zr!KXEVT($%MQB(6<;Px!TLjivX=Dt&b?a$rTJ&cg%83#clbk@JLEI%R6-wY%eQwluX z39*l7#+nx>l1P%)!sjrMZvfjMdGEW<54JX)uIobK+HT_1g%O}DW&$hF-d$O`G?W#` zz^60{(pphuLjW3&72c1pj#`^fV3u@LPgInK+%baEM) zRbLZqYs}1hCj{eJ3M4~ka~nPm5h0=3ko3-7fXDM9w*aoyAe~|Y_5jO>V-6g&e^kT( zvy!cD*wuv%yQ)FyCu}~Hstf+v34kPH2=(9v1@Fx3GX_r)%(ZD6{|xC98u7H5=EdAd zR_`mj29QRCuYyS}Q*t**GUwssx9DHB3v2o{6QIGfo9Xn?n(t%~KXPiH($+9vTq z&yqGr6P2l4-Q4;RXW`}}-gUKJY4j9>!QQON`m&cnAwF9&)0(c(aS71~pG*S)1jExy zY*ju43eHl6QMtS;p5~q9n0(+mHx?D65KeD+(6g-4;=}l1eCLDPwTvKwPdW8*1l*86 z(>j=4P?ywQfXvmrLV@;aKG|jp><2fMf`n<2Y=-|KJ5#wrC)fUD6c73p*Ss$cLg!x4 zqxBZvL?hA_pO`QuV1m+s!0Pd}+#b3ue2o*;RmT)6wqk+S`mAv#5i%sJM zlay95EuQI_@Zkj$bMutLtwD@jTPHp>RTx;BG+Aex&3!Skv^05pQ#?TXfM(e!b9P^v z@!=WUiJmUoqqlWGS}7_z=jc)S^4>G*j+{tPs_tu+->{MicHgBJbcr=l_;|4+=m>-R z;YUnbygwI``+rwRzKMCH16D*;I86?w9NhOTter5pOW36+-6{GG=Yo4>+WNIcj*1!mc|B_)_Ju!1Y2nEN$w}>-KHOy- zMA$WE;niVRa^1koW44q}7mysTyJs^9USjy9s$}-HAjyjZ=Yc(3>T#B~riQ$o`1!S- zl)L=bkZ#pzRrNI>RqX8D38K$f1)HE(^4I$dMGPh*oqc>KvhFQc;UCoPom#LzaPG(m zZykwMU8_DX)1jwr6VFtSEff$6$2~UpFP0X5@lITLf5UFNv&VYrS;2-0s&<>1pyjD- zW80SKdk!LzG)~-hWAjZFjg{F;_I=Y8HcJ;@S$wp9WpQ;@BZnixLVY%I@xF=OZj=fn+VqZ>HEh%d-s)Oq`;RcAHyZn&{Se3W=PUDPAeePAR0Bwn5PxBHU9bwTt z=EjgqN=x)YWtZkLu}8Q!N}jbFd@N&W-;$`kCnk!D8KD5%R2PlpeJ_f=I=h!sCNG9( z3Y{%#A7oJ*Y3J+kc%|c(wX-a=fKH?J;-J7{kJfY;8=E6N@ie_Pn@#tgD|xN6&Z)Iu zah5ryIfm0NO>XbS?ToI|H|sxLU;tvIXU5MqDNb3w61SacQyk*W}ZyY zYLe$hpLP5nHVS<6O?`KN6&jj4y2WZ&^mel5C7C;)v=bK*eK<42w6j9oWN3UkCvAR8 zuw5=lHhbaoMD~dIOtAL!`@+s~*Itnc-k|c^Omwy!i@KfMUt|gv52hZSisQN$d>%on z;qoAYh)Auz49-mQYXy+YNK%fnY9hIbOI6#7Lb*jli}SlV^NQpWIF}r{QWiN4?xoh$ zemOHw+oq2BNbIg(#=gr*srtV4PF!3&bEP2;Qq$Hm0r zDn8ww0)Ns$w`NFI!q+hp5{bB1P%#a$YuydkCfN-sjKdz=!6Rd$Xyft2%S1lY{|=A2 z3(z$rh%d+pzg_Gx5LyoJ>FQAMaW0h*va1|-=d~-ySz=>(%T_vDI}klAHnG8+KB?F_ zgh*6;;l^mO<55v@QIB<({un$*9C;Vx;LqqSf9f2t_ed4?+zOu}KuS<>5Qknlp+q(xc`Tqaol}b`1l}ad*dMT2G99Bsxu}V@B zA%wCdr(sJaNeGqmmUD7ga?WOsA%|5CbDZ<}usPf8@VoA**ZK4QzQ2dxpZ$S#@4BCd z$LXq&HNky)Abi>8Cs&nS?pkqA8MuUk1?0s3&f8DU&o1%u!aF@UvWR_Qak``Um(f#Z zZC%q{8Y4V=TFMOL#Kqq(#a&IxRtbdVC+M7$f)r#KAc(?7cgOzRi>=RwQvKC);I^)GM0HU^{ftCa@R+LzLB#qcvQO+O5B9 z)c#3?{Bk1xC!!5cgYni`BTVrN1ElFY(iTa5Q&N+hhOr`Bo1(=xw>NK4JF>s1G}Lb9 zA{_X!JHu_cIov)a6V+>Jrw=?_{nLgbfWO|pHSM9I5P;E1w~DgnFf+)MfJV+{Lxuj=~v41V;B zimpg`YcsJxx@gaV&URQJ2D`9D{esv-00aP+VICxofUvi0kgt*6Fl^Aag>rfi2jGQ4 zDF7Gl>UU(Xl$%)j3n~BxM<{Ot0?_)?zd~}${y63boXbjJ5=E`j$escM;HCn|(j3Ix z3Mo}6)w4a%`CU8R7oW>&GQZxa8_||asW~z&Mwv~>^Fi}*V(vyBQcrus8*nnF&?3zc z5`j#%om&~ctV5gs!)Wo-+Wc$l5jW3Vt@&7-qOguGt3D|$$36c@*6p9iA6OxCSKlf+ zcDJ(2Y9_BtK8KyoVzIjN`;~}n%*SV6vr<}IWN7Vh_pg&(%*RVGM}>A)B#k;r6w~Lt zpk>cz^J~DUBW~fxyTOjB3(@H-cCIi0wGxTKRMf#?|-Fg$o`8PF$gC} zOdSlGzz=PeKO(cOAYt+g$#o$W$BJ_*RHO38mU2oxU#fRshUOq1t~&Dwpe~jjILnDh zYk7Vy6s;wYlU-B=IGq2>bwYR>>Pd#{(*dHUx=GOlJWPT`7;|92Mb6%=36`NvC2LGj z7Kj$q12Yx%H& zool#ks0IfDtc6fmMdXAZS7QzgCpdxDWj8OzfUCIq8z$E|cXB$o%blc_ff^kj91Oae z(C|VwC3PxG3_s}y%ehvM>ORf!QT_reDZ6}AFwr;YLGl`nZD0{dsISM#Yt$z)fm!0` zchnNEIS^ACwO{NL*YS?s$-PMVU*VRYyC5j2aRY~jdPu6K^=u-oCLKM&)ZiteTJ1S? zyc)=g7tzH`%#ixb_d+$buES<>!~=UhE!;*Yo+J+ikyp=kn`Z}D0z@clJC_`u{r`a+ zE*uej%M0)FqNbwW=z@cpGc^+f zZ^E+O-?U?IMk&a&=E`^ntROKvq*MOs7XG_R`{#K=wDaGX3Ig(H-$mc#7Az54*4rvwlhVVa~)c;EV z{U_-;;t>5mw;O=OgQ){LEuYW9w*Ws*LD3M>Qdsc%H6~sZ75!; zH0l1uHXX>OeFh$+3UKA*??CLN152Xi7jGbgXc<$k>IY0uipYb@1IX-YuGszWf064G zz4KLoB0buEp(o7P*UXr}3@9rrGh3Y1$YYK2m@P>3ai#pB*2jU`*bf`>yLS*27w zE1LGz>&JeH=)SvCN+-==l-d&iD5%WdRj7c>HRmV?@UahRa*+QNYkXBY|LLm#ro=8M ziu~cjPai9rFVR@o<@wzTP5&Sm@*zJYBq(ZvmIG(j4JvQA^d#{@@qr^;g&yR97N>xG z3gA5aRZIR4+8(C+R7pOUIHdY&=%EnsKsqCMRM%<~=nz z&9s#a-S78Cog+&{%?EQNxZ-o6r|W5wXH0yAyWVosGFo>##{A5yz;PEjQ3E*Tw@~sr zbM7&IEn(%$T-Gl@=>Y1b7Jt{*n54s^Nt~boaSC(gZ8sth}`9i4?U z6yMO)zp5xM!}QU!u|vsmI{G-j=T?kmtu{`Zo7w+o_4zM%19TVAe?}^T5(IPeo^h1v z8%5+y+*pY)PV<7K!y&nOzFK+pp(xH5i`*Glt%yIZVW^c5>78Jjd;l+GWx5pH2gbW1 zRTAB|a?K#^&}!g1&bn>0(E;Q?IGy3qTTb`-o353p5E?-MxZBHTvjc)>JPeC^4D`ZR_S=qNGm zf3j8nA9Q=4m^0Y#XVeC)po|$~M%%`YhB3jnHOUi2L(j}^%bxrURVw>qM_3dp}uC)i2J_lyrAN&6Y4guumzJ6b^H3+|@sAoe(&G1&TniJR6 zy8t9TwM<^Jfs1@{7zjn@R$oQ2X~cC2lwjh8q#W5ohYEjR~(`|;Ajr#iA(4E3XdzjW{#H23#kxV@AyTdfa1+IlU;F=I z9(j$)|Capc8IYT32WeA#ksw`1-ac36E94kB0OH&<<2PpUOT6wvxgGYPW1~4O#P{f{ zpB5}Wv^dJpMv`GH)ItHXgQJd>$4aoJmdZp43&-aUTfKE>C4m&BKS^dF72f|NW=^Ga zl~P*q{a;xCR-Ae@!Hm!3L&~UwbvqHhpLfWV_l)6|1~#4Ih^*OgOjuAoR7_>$mo)cI zUkN17Cxa}XpU%2p6DowM-o67Y(I7vh_g*{FB}T*DU1^CLJG_a3;e=pklV-zulY z4NwH#pShRcPbHve73Ki{;2yAPEtE5Mo_@)tic5<9Yb~v{_N^HZ9LF!cf%Q=X{V$7@ z@zTd(tTDkNW=Eogz@B-KNcPpc#M%O616*4LLc$>J|F(?fg8FOhj;k~b2e(3-OoRKC zfwKs@vDy503=g=B|FcaH{Y2CAVv!otg^VNKccX>qBHU_t@83Q&x*CZ&v8Y$d9<0B+ zuw?B|SAJ?lEAWcdcv$=zrEvoHYivSH>=eq?k>hOgD)0USj2L4A1h1V66<}e%UU6&!R@i!$RUt< z0#3m1StQ4Ai`~F|LYi{}Lr23Kh85d}`vAcW%uoE_{StPk=%jOBoi_W(hbv)QVK;>HhZqRPr%tD#B6(&x-MvTl4sN90 zBi$o>r}p3&sX_MT>}bc|xzPWdvRBHj_i-CQY6v%)45059JGL2J+ry2j1X5J7mh&Hp zXqur1wJe5_45bli`#6eyTxmT) z9iXXR<=;R2Z&lDg;w=Ags2oD6UA8?!Waso(4PH%)?iIAL6IhD7LP%cn}bCY&=&d&&2bQKA~S!pvYcXDnbX zKK~pPvbBvHl~YsmdWvd@oeFf+K;KGOFpdI%P2$cyAZ=fQ3n_tf7P>xUiYpKUDZ77? zEq55QS9G$!x5MVw7qz-`?HHJ2Xt=#8FTo1uGC)=^6qd}pdAXLiOj%41iG4qVt1j9sm+El zCklk~mkLPCQUaY>Iha-9RAvNU%#||nBfD=#oE9sZ$2Z;M0(^f@?B@a8?P+Y{4sk z_OWTd%wI24bf`ql`xwS~mqz}v(N8l&tsg_N6F4o~4K95b3 zSf^R&*92bU%#;h=&{nG8;a>rNiUTn`Md;qK#<9R6m%iLe!0`}lRsvVaUvf0Lhz;cT zz6>>M*@`pVbg^4F5$-siuA|yXc2~#MVW-daJb$oCqduXv=g*{=Rn5jOSNy%H4AG)v z+M+zv!`~+=+t7V|m?Fh?`am5gZe&xP$2zX@P0V^F@X^R#ASxIEAJ@HcCocG=>;lq= z{!$W!fSwOzYyOKtYd&7EQr^aQ#_Tu|z4&C#l`t=HQaf4Io1esfi)(9*;M`sO>fXDb zgDE9DL`!(FkbQqkW&FP|et`7C(*rk`Ts6mEEEG*%%~EPRqF8}?AXYTh#!=pmR@7fu z`~KsF3l;a^hQM(*YVz=->3(s#tmny$v$NQf>+_poN3 z%V*^iK32WIIe2_7g+h@soIkCC3LX?dvwV)E`A+;}Li*k0{Xb%OqB_LrLkJD^Xjbtg zi7=ej|G7{8FLGK>uUIBY_miktbgeYC zV1#t^c>Ra6>|D`tS^#`8<`yF_P=DeUb)yidAh$;k%R`=}$Fr>$BACm3q$TgKMH1XW z(y^-OWU5;N+}6O&Qi1CcO$q)N$#Mf@z;piXjEI1l+lX%0JnC}uC;jfY=whEC&At@1 znHbShqgtiZl?g=DPJyEOtLtyZfOO)$1kJ@eYktbUeoqSiy~06>7EsFON#Ij*ly^J; zh0ZjM$@~Iup-hYwHPCI8$9I8OSY@Va=)Njvgl2Puv%4Vhs>|>LkJd?KnUY}PZO3Y} zwFW~$E>))$wC-MB34UmY$tW-ML;nmA{O6Gx>()NmIxfoGbUrpXAmMAa}Mc6`?~`j==Y&>w|)12ICp=KV*E*fXl2mI zz9?o44$sieYTe_ka6TfU)%cIyg3*Vy_SK@Y9|8JXVGnox=(W*l-#s{$2Bhexb5Z z7~`#dA2}UkoDvJor2^U;7c`-%#~$vLZs$C24@+F5&z>L7Un?*7Je8IomcIo+N?#&Gw z!jNI}kxm|JR1-BJ!HV$}q|>Vrs~A_V3KKx63+u18*k2;0Un#dxn7v5S?}PT=a_n^; ze^kFFUT=yVV^6@4O}c2R95ws}6yKL1Lo2XR@8g#%ygh_{%IW50J6~mhtg#s-KKubL zeN5)XT@@E;dGR)_7_evOn(TN09g2T*#b|lZbQr&r_=jKk0$Tw}nx$l)%7TRC;gHBF zrE(1b^a7Fvg*53pXqO)TjHu3s-XR-JUBZi9j)*Z!`_P`5(VVkRJH;QVwwHnHD%Z=! z@c+Nb#Dd-G!7WV4FL2leSbAL-{G2Ql?fdQCsveR9yP0OAOqBF|6M;SEZ;EE|y-+LC z_YUt}4UA?Zb=4sM@|T0<|0ZxPbVP7okce6!q1lHX_I3|^M-(YNn(*Ag$$zGjNpp=3 zW7bhbGp~+m`pv&OIlHT+F^2i%OJ79%$JIs*`783OeZWc%P(ocf_leJhH5c9VI1#1< z#nWBm_dq1pv52ZGzfuq&kDr_V&y1~j_D|FJ?|SvgVDzHRVrbkTj@gn7V=qE^;MpJ# zd>3L>A(5ub8J}z|u`C7mB#RbZE@GF-747pI0n`dL_}{pdpGGqf*|fOux;>gRF6DfI z;8qDdLF%PZa@;_zP}W0F)|&(<1Wi63A_U_C7P#?WY7wZSygQsK*m0-sBe~y}oY{Io z9vu`5#ZGdNKD3{?JHM#O?6XleR3#hBK^o zo2msYJQDvyh~z)@>Q~l&P3QIH<@8bDoWb^W%Xx|O`wqhvb8QH>Q2ERh+T;mhXs*(` z1p%r(xvWu0rm@e2iwXXp=O}ZR%wIOL1)Sj3Jg@G0;v!QX#kn!5aA55XfoY&gcV~`# za;>n<6AqfmRqT4IWb(tK5`K^WmqeAU=Hri7YpvvOkUh1qXrXmMb@;#wqu)&T z*&7j}TW?DScds7#G$%sIoCVohzK#AO7=zm7M7GzzGB`j+#=&yj0hS_K+t?F;>V1jnyZA|P*=_(tZTk*!E1!-_4UmAF(_1Y0^S{ho~og;yLB|# z{a`Zx;?ULi6nBLlnd^AjtxOj^(YGBAZW7wG9AjFz1&HyM61v|a$dKhDLxabjE6&Ky z{5`$!Z}y&X{${CE^6OcNdC2GHqL!O@m!jyKtu4J?l3O2B zV8Ei660iU6zu^!5PM-a;i!1}VKRqSw7)?){Qzb8gICIjdYnoAbpyZYn6{N^qAJ-J- z7(Q-)p?INQLBEi7!|knXzQ;b0x-phAOo;B_C3m0amK$%St0N}f*Vzhb64yK(yW&G*_l=*#ptFZo$RLY ztQg5D-i1<~SDeCrZrnsDK^|)OSUBnbdbO!4p? zS8_#CfltK67;>yLsRG<+1h+l7TLBc1f77vt8~c4^Xd^u+wX*fz_j3~Y(1{NHDOCps zJSc?cKkLc4RjyDI?eLkG_W8wa%7>lI_UVp&}s)T1v0hW;-kG&nSj(g?DVfN~I?9w69 zje~>AKCX{z$UnNhEbhsyn56_m+2#4dAToM{;3fnDpf1O4GXhZA;xol%-Fod$-8$S+ zaYn)f@yX-5vb7|TBdH5Q0bSEOM}E0#Fp)|^pGQli?(Gk&8AxUi!v!@tpIh}rX7xF# zrXZ%TP!atF$?v#+Vs9^gqIOM|ioo$$)A|X9*|{{J`J8RvhJhW{b!qOl9t3ejW?Fqa z)M3#her)tSWwmK;{D+;?1}~}?)kVs~+#} z^X;~*5?TF}bhz@>rlS|k*DrdnLa&?IzHg(-VZQCpHdRG0PzaqvX3mq>Q(X80fbySreIhaR{;lJDgwW7QTJ-l-ClK@R7HoIwUl{fhYu*{Q{j9f; zy4qK#&2A^oY{&nGynkR`ChlrLs=5Ai)nPHXB(X2mM8#xdRI+%%#2vZ1vBAJ4^z>8l za4}1feEhQtPLVMsH^>ovVm6+2{{_=KN0w3tJD!Wi-CYf4C$o9{*@&%035qeJom@Gdr}f$wxSjoH{C8=?|VGBh8!ueHOm&#A0GAAxncb;ySO?9H@ z-nEZ^Yfsj$l*qbg)s8-1;yddUDl>k^WTVsOm$9xE>j}bn(D!5i%5-LDddjSsp?zPl ztes4f^z7jWvcuJL7F%l8G-d7UF_R@(> zrgu3caJJ6w>%dUXEKJGTpl4SFU_Lf_H+oyUc3UQ6iyn|}h|NyBP*Ce*&XD+yRGg01 z$oiq?ej3&D!XRMm*6mT@2ioEmpUY2Hr@xaS`(2dVc7+uA6rHZ_A1u4BToO6`wAoKp z+iQPV*yn2@veUxQ33sSN4o*!w?Wpd$qT$8INjrC;ir>6!&Yi*-Q6nM;dIarnc5D^0 z!wR@6on%a^>3f|~LlCZPj8&@Hv;M|Tqq3w}88d8IvU#G=1Gx)d-mBTS_*Jnj_0M>NO-zd3d;Eeg)@B z$i&rFg@9yp{b+@`m$I|^9S=@oT2@Vc8 zoH{9Y3*m6FMD)p$M&@`Qk$T;XXphW#8JM8pd@ED!8{KySUsU*=b!et=T}H;Ng1)2v z9VP!PUYi#QE1}OIWg}u1^nxjb`=LBoYv~<1r!TktmE~S^I=YlX+k)Lf2okS}DOWQX+T|yGa>q$R$d#I0-F#t;6s&8U>NqU+>Duo89}7 zGlz7-ObbyBS^{p>$*4WywvnJtd|WNo!g|h2G0!0|RytYiYLeH9mQWXF6tbMI-l~7N zneeSG+;ApzXhh6x;8Y*Cl4n$D*Y?@zK9E9AXYby2xVq`_ z)v7L_V=p0Iz2O%ut5SY!j$q23N0_lXId3Qj{lyb)M~gQ*-)h$m^XyBS(;!-CD$F;b zuJ%1hdfZRkSS>Qu4mGXLvfi@5;psRbZq#H}cOEL8@{M-KO5Uh%3|;b! ziO$3nJw;bS-%R^n&1c}JzA^|~DmI%G@Ya%f!c6W7P$Y+Y3{&HaPji$qo~dSnEKzZ_ zul#PCgz3_}GqK1!PD_ny)(d<=g*>;jw(EEp-1vS;Z7hJ3_#pkdzUk50N1$lzYf@9t zM#(r7+|Sosn=ZyfPMfW=tZZ2$CwRP1qU9Pg^u95&ORbmTmU)JwjjDcf>{>(gEUkjA z+h8GJ+5_t~tWt?d6fX$xz@BC*UxLGF4wc2)87<1!f`RT$c`p7j%f;i9 zz&I;+;pUV-^O;A4AWO6N?X#J(civ8M#W#bm)twZ?x}-6VYh$LyhP4m0Z7~_$$agK+ zOqg$jiJttOiq|ISvJBaI)nQwGWb@u>`vIu$%Z=d-sFlsr!ZX!hBTX*B6nv;!+|%8) zqB)qU*H{)RmP&!`CkK8LxkfR*E;b!cysVt)ZtihD^TlBpYNpyl;&ZxJA|KxJH^&JL zh1t#0P&erfNU<@Ab?tr2KaJPxwH!{#QAmF8+KWUD`~Vkp3dpUYbo4Z(xjt~$d||a0 zEj$+}16oRuNr_YvpZZJCpKJRDhK_^~Q;>|!?CS<-i#zU=IxNNgJ7?jAW~6lrPYO>B z#k+>2Vm>k7jzFI*e8ciKO;u5I!>mhs=67$%ay1w^8onuJ(5U5yMEYBEx{v4Yi+-Xx zdH@=Ykeex56Z?!dJy$vlhnL#iIiEp;AKbfHr9knUVptW))`vOM==(regnd0U{1v%! z!z_J1%*>Und6{~!_x^0ZC_;n2GR9>oo<)_%UPOzA;gpb?C@KEoyRHDDYE_w!K@U1KwsV*Pzpfw~Y&9>4zbf2SDtWY2AVAUK!(4%wVN(sj@^O1VZ* z8S%DDF=5O$)m*HF6PGI3kxC$cpHL^JyW^Z5s?{Lg*tDjA^DJ zUpOlgf8gJlm{aV~u$pOVlyYYmA`jU`$-;OgJ2e<~74$m#wkvmx7{7wr`!|t@AT0Yl zYkNt`o(JryXvfnQ3cQ5AbNwP2D$+)@=fPVsuWn>v+)rJVq(^;7mIqHRSRE0%xNEld z9(Xnalk_*bzU2FaKErL^?&ZtxhlF(X_xpu}@!B`IqItc|8TR6iiVxb-7Um6zh{u!% zw+B>KC1)D1xPl_+`FLbwbN4ur;bz9@3F;)5+@X+6k9219^eu>{JFo*Bnia*F(4wS0We-6Iq7O}~i zuhpK4I2+O^pj+{A+WKxQ1n6>$gro@fr41Oa=3isHi$I^qCp+JuQ`07y!!oCu_IiF1 zQyC)WiaBP*Pqx_RP1-r+6lQ;#w;il@%y!)4kha6)gG{IR456#*FidetdZu$D)~u@v zwh&5l(Cv%?sDq8NAb{oIIqgGx*43j-TmsJe1XZ+E+PVm z&0cHLLu+y_ReK1}&F&pt&@GEJSz*2RKTj&?>tU^*2W!Hd1y$3XGgyJC#<-Gk;e@fG zzQ&>4nPz{PbqW!$^)~tl4M5N4)L+W@#vcu#LrrGK)qA>1yn?T(;h}k~$GhPyd*7%S zycA}?S@^-4pzv;=B&gV7UIm|JtB%c>jzv1S_MuYc8H+YrTleDG0v*gc7^Sd5vwJe; zV4+TPy}q(UEv5B#=T-FJ8SDHnctLu$<9zkx8Po{JbF9J4ZUVn>Mk>ZZ$e>e4W7ctS zuSYt42xZUtHY59BzLipEAn{etWRWaxGd-RJ)%k-8zD8M;>O}V!{fIcq)Ly3jfNIBC zj!v`AgLXHq8Ggc-2BwOF$O%|>#(MTpir~OLIc8L)m8Aw}t_eqE$H)5KGtQ54E`3kP z!!RP!IzP8RHBXYVvCO$2H@->!sYs6`>BZ6|Mmh8^X+wC6^uPg(K+B~;1;t3Ay z9@A!4caV5yAJOu0HjE{3m)WC5x|?@nC?d4htKqIe-QxJp_ZflAmj~)6z00NFIr{mW zUL|AHFS*YR46Q;M?y)C+?q-|C9M0YUxE2hQ#tK3lsMGn<;sK}c5PP)O66H=%(&4D zUiKqCK;t3QhWTps{YWQC9nH_HsNF8|_f_%F#>8;O8bTbs#xP7RO>f4jcHY&$5$PwO z_tvH_9b6Ea97DMGX}{73QRXegRWbMdM>zt=;Do9Pw-@S|ez`{&)11iP77=maX+S%x zt%=2+OMYtV!8Ikm<@eo$2xUzc^*V^VH_3H{!~zC$_FhX{{1LtJx46ZUz~&NP3a+6V zMfTh@tF~_*)$o$oWEp`|=fDfYh^bLlR7MFpW-euRuz;-=w>UqZHh0_!c~AUSn`*Lo zU=BUn%ZuN#A8g?a)Hn7^Fi|xpud(T`KS45yj9O2RdY{alv_P|QdonxQ?+yT=^_=pf z-oC+5Kf*Ijz(#Wkp+3>pq-Iw+FoeQdH69jEUud%pkeufnN}I~O7^}5RO4tn zB12@E!u)=#^&w0_s3696FS=}&U^<^)4+g;ugVNU`aZ=E3^cPkex5Z_ zzS7{4H+T>;--aTHELLeNIV}>7yMm(M87!BCqUH?kjN`N4!+(8}^a%mwADoCW zAJ@9%!Agtevup7iXn;kvF?pX{^N6#@XKq@}-Ha3NlWAqW6^5IRrFq_Cu5-FtKU0#M zXixVubbDU4)obsz@1Zi;fyoC(wF>Z!6(L|Tm#^QDh!@ zLbC&Zz35s^_(uh`k2BLbQV5Si-|#L6mdzkft4H7t>FUw3Vq|I<(I%ek)hS5om~srY z9#%}&6s_4&;4!FW=0G{hNo;SbqHpzDeclU{mDb*KvqIEkzHhPN-x`9Hzvq{$XXNe$ zC6T*hJjbZRNdv1!V*soKihw{~xmemsWM$a<671g^Tx|G@4wrOCrI+SUHOu*XP$(ap zclJdcV`2~5tmatm2QIBl-Rb_rYbgcPV`H@9r;oO)22Kv&*pj{;oG+o<(Dv-r^;%8J zsJMJ*0(kutk4i&8e&4({f{U0y^~jL++wJK3%hdij9tEKs(1(TX z)UN^$6f9$g8Pvo$bV|QX+H4p7i>QFv^_z6XnT22p`|}!it7~q)n65P#h;qje2fvJE z6w$uQh$C5-wD6>)nsWWZ55fxSk!FS{`jKK{-;iS)Zim5YLpPD&*{bSou3S(40j`Qu1C{Xjfao-AMt%xIpA zGQfl@x=zd1OfTt+C$1D4*VsMDLk=eJd_{NDO}aBansDcMqdw7B|1!s)Ld-wsGre=( z0e~mQAD@Gb{R0_n%RU}vbJizl>p4O=;|;_91ZeE2yRa9%#tYZvg+gYf&AB`rui~HY zpf@8ZPT#+dwz<~HuV>3T$%=nz7ZNnwNuO>^pxkyJTXd22nA2PmUwqPTlD}k?r$SAN zKSSgRDIGh(_M_gMlNo%AMtk?Ef0B~$R9s{F>MO@Kyu<7?!9ZAGRq}iG<6qUZISl{> zAB-Tdk0wo3OxL6_8Qw-sOPOQe6qB&EH=fw&j7a3`NL+iY%&+laWB7`TQ*0iuTWLC7 zlMn5@VQ^mWs%7??!ZOM*GwSLM;7)KwqFPiFhwoboQ>P(a!U#^fEaP;N6)ZeZSw*9cA9K~!&Lel)B;wBpCNhquo}yBr%+J_aMROUI~Z+`bBbADEKF5eLeb(jH#b zJc>b{yrM_AJ}f(ZIAmxp>aQ)Uc$aA%QYMU645~91LBIky_OH{{UgGyij-2}t2)Hd2 zEX;3elH7Z+&3im|h{~8~KEFgyS1)vW9kb5A%^TzX2~Wa1qI%FUmkC*a<8r*0*%EC@ z0#C}#A%%yIY;WQxWr31f(Qg<03yV%hl_YBJ{mpTC+@$|?+_cG9z}uVpA~!QW7>fpR z-*nl8Q|z5{gCgjACY=*vo>?{ZgfmXc2ljmsmA|qu;p%y=@J%~z)!l&9FT84u>pR=c z55Y3m3(6Gq_DKHcq5dcQsGIS`R?<6Ic6yUA{@d%0^~eV~w?0!m2gdzjEINtm)lx%5 zJxMy>LKEm1e0*~OVKW%;KvwOZisFRaCEjtxiI9NVFYe>M56STo4`+>=>1nEO1o+lE z)+@4@am_uOw}!fpN)@E*)`4CwvQ^a}K)2R>lplftAe2~yyT5Xab%LO{D@)H6M(;E*zR3RNnl7O4a0iv z3ib}xCMW1j0@`Oq{-g1u_IdUm@X;Hz%YtU6zmMRNyM>8U;+T|@!g9+~`EUqb6#jIovaWcM4XmE;m0`euQ-Nq|CmDJNMx3bP^=FhLImR0CTIOy^hAL_R z_|^s(#xY9t?5s3EUV{NNj-LxOs(bm~u+J`B*qRIUb3cl!=No_R@AjHGNU&19U*4mj z-7y#oT5_zh=-v#X?Sfuj$Cy2WEs+%c<8~ZwLzKK1R#RHSU*e1Av&H1Amej}7`rq7k z4wmmQAmyLpVxWF{w_Fb5!`N$l4DR!V^L?}(6EYh7G1yI~d)x2y^HxqNdVYWrly5BO zr#j<48eHhfOl8QB8q}QM_^-?;@q<<;kpjCP;AN|OvoLLwpj;D0c+Tc*G&9y1#Lp$; z9J=NuuB)NbY|V9Y73-GrJV&+d&1Ssp<=Y&)56Qt8_)ftWlpB5*AHH*9tC`(GsrUD` z)$4bJ+YzN~T7{PDZW_Z zXn!wD=F-J*J&ETP<=N8Cc*!`jvk7oe{*1!NwY0uW7;=+NpEjmLMzX)6w`#v22!6AK-a0b>&Qi80p^A|LW%&YNIn@)$WL zVu()mCrA3#7wxf;@BI+-#lHXX$aP_6YQ??BJHuKwiRD&pEqLy|Wjo-U=a0)+b)T<| z&;fgzApGa&D&W0)*-OY^7CuMP&f8ABvpvd0O%2>}~N_!jZ*vc<~v1?7we4~hH$$viuvG#Zcuq|GN6LDH?=oVG7*e9x~nrYkDX`MpB?oF54&Wn9h`&i>SH#?5F$srJJHR$1;$E9O) zc?#Lxd5FSXuXd3vymMAv;7dmr<2|I-=NNq5-6tHg69j`$8tj_MoTja@zd~4*78j%2 zI@`ZqBUy-bF@56^84WK#CPW4chsSJ20DwXr*A@Z*iob3rFOXo#@3KQrcBG&m8|KIN zoevGji+OlPW(*&{SvB~W+Qq37xhiPBp}g=l=sTkTJrsZKANHp4N`4HTX8!a#ML*4i zjXl~oB_T{>CU6OFUc_-daj1?&wq&|?y7AEhWft@~fil#hO0?AIol zC-1&Me%ay$pAI?7FRvk%#+Ex}hf9K%p z@F!qYo>nXRx(1qIAxt}dETDHyDv4h1i+^r=-KBeeXwtp*YuhL>60wGWrP~w-64`OU#3B0Crmaiqzd&33=-K}Fm&DHVN24p1F4e-i40a~~lA~r<|EU+zG z=APR^5S!68SBJg4^Va6YX_BVV?%x9>(3v9AhxB*_&}BKi5=+nKTnDbZcwe8y zMur?e>@N^5_#)7>+8C=czp;=6&|}Yg+f+pkRO-(Z^5F_68L*>7r=F!i1jcJy0A63E z^ubAuY^{3J2RSGBKiKG%60c$^*5+>p=#QqX#?14M`Yky-FZS8B;OA(!b=up0x(`$NWPH zc8ttD4k>B7-HV_(MTte(LDzZ{JN=xdcQP3CWUEO3lBnTaKYiuLz@K1x{9e@%SvRhn z%5^Pr@CW`%6%6jRU)d2NaQ_pmD9YMMUJz&>>Z)JjlNQ$h!dRb0U#cMd0sbS|dw z^lENTXf1q?5B*th)D*b;@2-rXlQ$SZbfO7~2nKBS2cU1wG%rzlYAXGe7>I3PLBRR| zHQ9do?r~zr7AM+zFCSx{?C`a-gF|l%JMl}tBXyt1hi6>hAUF$!q+^J;oYrwLy_kCZ zcrYlI-_T|}^vTkUICM#o)IociDq=Dnp-yErU5rxU^sDOLegD8Iw4k#qFDNDUh?cXe}67cgo$=au|2f*6^IZKR%%C(h&&0lQHF#q=6=Y;;Q zD0~GDz7$G~bbi{X$h`LNgz=m2#P~?7g(@gmu{JR5m4R>1K z1|7t8#;iHOf80Oy(idciv^yL$!Y;B3{_qf&NFpp=(K{wn1@WlbNUGx3tP zTs8@7$3_fg*Uv=}LTsMVQ9kahID<*Trx33zSy3N%fVV)Tq=s@}U~8`r?bIP z3cG ziQN*fivQ|Nzq74ewHtK&;I~}PdkP>2bI*WbAkV|Qrbo$y>L!*}$@F;?fW0~6&m%}+ z_Kxwo>gb$dze|omq1v=8h7DJriPA*gT}6Rjxh*c7IYEWhM>tX!=;$9PcK^ zA{Vpck+B?WVUudE7_x^Fh8fs}FXhmTJvt{t8=?m423v`)9=;%NqZ6;ES+iS;=uqXm zyUu{uy2YC{?J}exgGaIG`)p{9k+IlrW#>onYQ7g z#Tl>auUVR2d*0u0S7$dVi#1KA;Yl4!GKc}qgf@{}o_S$)d6k}=v1Dd%vXSNymWflU zbJHBrjZv~B;f6g(LKM>9&y$LV?W4E89=ffos+Ro02BR1Dgd;M9hpJH(Kre24Rk2l} z=GVUie?S8M&Y7l%ThYJ2iYEW=v8piP+~%+GJ>|EyJxWWDw&cIUFU+&9xIuS&ZF( zq-^hA22s$xFJpCTOT!>71Jrk~HO$ha33g62PfoE?0O)+f*yT+JkZN zp>Av-5!vN?b!-o>II>D6(4%RiFQU_C)_TC#)N^ERP?>|ZMPUlSsLA7SD~mMq@l;sV zFTpOr%6t5MQ;khH3I|4m8bA2l5-(Jh*09)yIeoc%)q`X!6wUgB&V=bhnQ?r_lTGxvoX`p4@R^LM;V$TSzb=&+4!-fxZ54a!%) zYhq;LZKLD9689m@SPiE|3P&BW(gV8N7q2PzHe82U+JQCINLw)DRbj((&N2^rqh;_z z|NLiy%=kvQk~JSG)@w zUpFZiQH2MoHCTf!)lQpj5_WjED!6#pkl*i|t4)l!%v{Qj7}oo7QGBD!T27H|W^;b| zYIxLR8y(H7*6U3g&TfFT;E9w7e`BDutGfY>mO#*0+GE$EK38?`%wjf#)-C8inWIGx z&C4G*Op&|Fx^wav1^L6WwyOibd#936WW&lO{C4DxmI>LJp@k$NSwi-G8?vuM_I=+OWZ%~@3}f!|oWA|8>v!GPbw7VR z&tK2${^$FL?b`FZeLOm)FDtc11BZQWc7lQ+(35X$9EP&1{Z3wu_)XNXsl=vN z=W%A{IL*Dmu199%woI?xe#bh$*8819R6YN=F>gkSQ3pH6e-%hjNw%2y?EVhj;24Qh zUw>o^GW-E>xm5wf*iIE7-UNoRXvOEWNO}Cm>Y2n3$ zu8F4$7}8D^AW~gxlKiMtS69L9INODQ9&&$0r|WTWG=(si*u)n|9d*R>VFY9+c4DtB zIN-0lx#SoI8`VJacx$>sHZ>L!0NVM`+r~vLZ<@$JY*6oY%)8%+2tDo@)2v=d0ZXRM zN$oD49GQhR9|nk7h3`MPCHfG`ZJxtTAsCS~?hDLr=kaagyF5bauY>meq(o3#yvBi} zq!Z8XU_)a7s@4oAQMqv5sC*F+v(7!=c&*w~2k15*%QOkRrZV}?G4jyEO~TyRqd~_e zD+IyqVJ`6PL4AN7ZWFVUZodp`l%Hk$LEk+pSsHHD-LaPY?Ie|*Z(FXX%Wz!xXP2S6 z*I-<8cJmk`sRT+)#`A8z(Pk{4Tf^I|lhfrfi?((ia|OvB$~$+FulOY)aC-AlwD-wyWl&~Vsr0C z#|@B7U;r>DKrsnUNTLD-AsG#9aV_)Hcg-ZLKB8w{-DY~SIq+9)^IzzXR*ZX`cPf6_ zX)|T?@~P}&hJ|;!hOWXTz_qPeG5#X28nUf{vECfo`7-EMyOv>MzH+{_rPHe8`RT3W z%^z=I6)8`zo;#tqJkbpv7B4`O#w*i^mkA&09LH#lNbCU{AYp}$|^{zvt-4F6$ zZ#_#i->m7QJ_^<0Y65H$Zo+`f9IMCHNN;k}&=?N7t!tRCo3w?45A zUz8OBx6&btg&9!=ucP4>#PaoD8>tRuFu|SS5J1yA%D=VN(yTJg$nFx91d(9Y7{~&7 zJ29fn55RD#{Qq3#Y@Mm@lM@aEjkq3iA}4{33m~_B>OCa04RMt*f52y9i`rpbaN$+p zXXINKd9$W1Vs>ILsa8SVFnbredMyfrT6z=aK{BmMx6_FkFKS ztToC+x{xx)6==A5ys?j$@i#WBq48{o^WqrYGqk_!$b?vQ3&Wxfp+|v(R(j!l z^sw%@NevT2!y~a>5yZHS=Z`Jch1khZC-m?b2Ivt(Ejii6sWn09-W)Ix{gihMo!Zs` zLLa-Q58TMK)F8HsFcLFPnoF(9Kd%5s$L+jak?z?n`qR%j5kPUWAdpHf;N&p)cP2W) z!slxM1qxSdNT?CB2uZ;eY1>elikg1=acqA4-3*7OHmIpMN>HrYls&yZFrz@?y8Yup zRJDFPt1kHQR@6Y3QS%<8ifh9?|M*r|_7{l06^*Nrb9Zd+KX>_TJk1#Fd5IiECz^6X z5PmrO(xY$YCYapGtikK2*Sq;P5!3ur*7k4rEZqY*7N3qWvkAeXHN59>UAMnc368>N zAgCW!-w*ZeWBb!m5*G{fgv)7$0U&;m_xHa^{{I{1?K1KB(7FR0xhkPy8fx$1_17l>FiCPb$?A1VrQ(tLS#O{cLZ@`MZK#lf$Gw)QQ=)WentTtG*M>) zhe?wYDB;&AMex-~v| z%UYz=;-Vz2u-9<6i-u0`>h*;29y?VKC*(rzf73Oc>6lU8qt$DeenV^uKvkr_;qN~S zGcc{eX0kk0=jB_DLYUoE|Ko@l3me-;8;h$1CG;JHkBQaq;ec=iHoQavMJE4&>-cGb;g&fFrc zHuZ7++zA(ejzB>onnZ7dGzMUAE(XVU_)0mN+C6~Kdq5(|U>jR104fX?*O3_J+JtU{ zW)tVg2Ww1JVz#J0paNRWf2n_)Qq4a=I`RHht6S0y3El(erAyj#59ATo0L#fcleI%_ z$HvnIH&=ikQmVBw91=y2z(Db5CQDD1DDUhRK$$g*)NC0KHeGnH3DC^%horRrACl$kWw_n)rPW~6?nGj0(}XCjL&$TJkKB%t6v6qX^8lt18#ywteMx|)CPg?o;eDU3}qaD zpw{l0WpW&OBFyB~5*kP0kmq6kF)l?@gl^^(&Bu?_cJ}?+l=f5nq;% z!i0-lmwDAsrO`f~@U=}Hlp=i7%Yhyir{}_7{(eby@p*FPHWP0)Z{&CFve~Nrt+l2* z!gH}hE5;Y)yk99ljFqDUDZqx1OaU zg<=#+g|<1o^31p$81u>Z(;9*@$4dW)Sr5cmd1g5Na10BX59~#LY4>WIvXuGY`aJ+S z-s++MM%6J69ycF9xoqLt)$wyJ=Xd6fvFM?N0uB5_Ic7LrjSW z6WcA=|IoOc@uRbo{P!7fHz7fP&x=D%& zbtRyG^{ap7LrnJCKJvAvdn?+?K9p|cCQ5`N3A zA{@V{SXPjaf+s*1>PZN@Pc*ln5 zSmF^^mNOdzXTqa_0}@@$c#HJ_c$Fm1@6d;BT;sM$Qq{9uYXp@ui9~^caGb5B2q+S& z0ySQ3RFd(wj@o|!)3Zl)H_Jm$K9F^&fV#-@g!A6W((N37Zm2Q!wkO*hydy7i4nN5TT;~d zs^64+ZlEvs3us^2gg9HxosDANGwY83$}9d+O|v_SO9Sv9wfylP{S}NLmv&`QZ{}~t z6MR_gDeKH#O*~&$4!ffc6%mLdr}ky&%AE=bcQs^(9y{fUDlO@9dtgNwOkJFk4V%CV zJ9!;Ho@|ifmGEV-a-EBfknUgtYTA~((EXJ^dU(14OA=7154tx0`u0#a0h^jv&~DRc zQ#5GKzJVt%-O8t`I=|;v;n8q@o!&h|wFUE)IC@A=m(t2PK7S!V;`musJwjST zj%WoRRVcsthjaRXpZ+#+Fgv_a)CAXH#RCpmrpDOIqTj`}AorI^;!^hGbOmjhlR)&V zi`k>0$RX=Q{7yk^a*-ASN!1enRzIpnTqhvWB2tJe4NXYUX<$wAKc~e zeMss1blT5@q@LQd@hr9IwRsxkHj%+x@#g=ni|3oFS5EF2{4t&+5x^i>)qe zih#6M0g#T*Q+o!t_M5YX1N2KySJ8uZUALL{p396GbN#B z*^wrW?eN+6n_FKQFW1HZEYI72G=|TddcYkA=nP($v4{g}8*}~-JybnB9HZe9cQMR= z-#-oQ+`HXuYdE?ef5qXKHY9Bb0Fcg@{1&GHb-7MNU2e-4yrS-DX!{Yi+FjA1?B&moM2gk!pJUMKLp7oRV*o9I@ z;jLqa#j3kEi1OJFam4V2FMvb$bRNLo^qmd(mpdhtNMHPC1wQC01{=p63d{6{#RMyR zn|pJvuPd3#C1o$mc}T-*dZ%`kMjPPJTic`ci+V{F~^ zdwLtKta0B<_E(2{s=EWh%V_e_N%YuJXYLOfe>h(;HKdD@30l+a6@IwpYw-GHCqTS* zgra44KVa7P_7PEH?Me;s?B9T4@9^4b5a`g_?6?Xrx~{U61VBXts%VJ{?7t}tuq^hP zIq$qdNE}pQE6YP+|C+~3Dvi*SBlmTKrA~><>|XVg2*T?i#(F>YoduX~5I%MF+3%uw z=>!)NiINKIx|Xn102dn5T=-L@X}OP8X%}cqDcEc;$Q4=MBVt4Pp6@VwZAk+9o;yr6 zO@lk?!n?jlF^>;R?@;wEp%*eaGC7JT$k*3gH>&XE-A3r-ROl#*a9p_b-TOd!=e|Q+>#J~+_lPk z*Iko`<=O*(ervC+deKe)(`DZ$l}~^nT)JnAHYz^QbH-W_z1vPxDpgSZJG-MA-F~S> zDophobJt6m(*ll-^0yOth~}XlR<3_6@Bg8C*1?#^CY?OYH9$l)+YbRoUTe#?<7oyl zO7_>J<^7NLGA2K_X#bzE?r+wf&Q*e^>3|z$1+$Sbr}AW_c1_xc$A@i@N<+MmGH;F; z0N_5-l41tN5V+}7pV5Cz)K@s@8=Gr$^DY^BW zNuKEVe)uKB0RL-2_;Fqg7bXeq`MkKguV$&t%A|Z#i(NP=BYrMdVj=ffg_H)d&j0ac zP4bH1>on|nLCZ`CQu64!{`YshboZaD_18yg`Rz8G>c)ah&!SErtLDe_nFg-lY7F3K zIkc=61Lq*Sv|2e_?FDc#A#X{}fwFy|SzAnSl?^!lf&@ByG26dN(aIgK9BfIQL`USN ztZC=5G-lko`~onYMAA+_O5oOu@6j$Vz%()CyuWL^+^T)oqjX`@J$o)?~ZDizPj_ z)__mHFnhVY^9*>Pz1HkvYsG5CWX(u}-nV`jjs`sM|4p7sn;h}W+j^C-Tw_`@xh~Tj zozRQw=aWqVO#Zgg39_UU4fA1aKwb=}6l%U>dVebmcyZ$$t62)-{tJcL%4j>DQu(i9 zoDb-=^F)QSTJ<4;NiY83F!;k#``fACJ@s8(9)|&4cK(cM1q6o&E6OX4d>g*HtD&y& z?TsnJh~4W?n~NoYY?ED`g;k{^*~;~DKEch{2*a*eSqKA?(oLun{&DqD;PrBIAZ+F; zoe|&yxW>E#l%=IQ8oxlm(M9wVihZSuyYxpA>G4m)2!x{yk}2=~4>)%R*XZ^Fp~hya zu~zzkVL@Gxb(QTv(T^g9c3_YcOPTN@({1ENo7zii08`dbP8@dh;l*CgxdJBBe6oLoeQK2qOW_ zs&C?5sFg7GaE`-txuPXKE(4oUc7Sj0TGa>*Cnn;wv71bYKAa??FPT*Sp<+J`v z0F8Ztp5s3dw|;s6sqL6Y1H*gV<{6^s=f) z6z^AV$62AZ?q!J5)R=ZjmwQ9v@&5YfzFdN3k5NtjVt>5!xICLv;yGHDxd7Ao_233=sEnWY>6Gui7kj>wF{tZ zsiN$wI%KP*A5#(M7cdAz@+ zh<1=+Q#An+kqiG5m?M!RAp^t$s|~~Fovo2j_zX^NrwVhg#w3x=dJW|VPPQF~>`O&5 z$ckB~Jee2PbX?lcB)YpDR_ZU=Jz#%An~WkU4c>#!rsWPhwpK;GTU>B+lK*S@^*Z45peKD~8wf4LK{bvJsdKZ(Ll0JJ>*SGNqepM$lSs4Q` z&Km%aoXEsIUpJ0LZ~s)5v&lLz@m@btu;`jVZk-O}x~w9*IG+|p>?q;^(TGJ-ZLHA| z?iJyC;K+jG5}}pt4?I>w@g*Dr2wm5 z5Lj#F6Im0&9(Zw&VByP*zctNM$!eH2tmSOccln1cS&Hb^@{p}$CZJ~6wq@M=M{?qo2 zri18v!XM&l3puC@*b8V2fPWXJwoJ3DGIZ(iN9he2HEl@rxbI2W^=VC6{krhS&w;1^ z#g#Ebnbi%Y_WNDtNM{=8kkugun#c;{uH#_3y6l&kxK*u+oYvSJUf7 zZvkusFuwd@BOW-NpE5fAhYkbkr|RcjqVpqO%sq@WZY=?QU-ATM8~ z65p`0ubsLN3y(#)HIVOlMO{s=4eGFe&ES}eT&Rr-K~Dw{pC|hgP(~`jZgwOdoL~rzI%qnX&mFTGOb640T zetTgMHix=YMqQlFq6LXaC3he4n45Inkh0TX*wT9N{k?Lb;zI?cU2(`o?B#YZO9Unk=1zjhK73h|Us z99?_`^(xeRe-IMAc`%werO^JwH0z4<^Y%BjUcO2Av)b!&kp;iwjZ2Sy-^Gr*d^x;s zb+i*J@a5&@70J2k71DQ>r2*E{v3qGkCVh-|Zj**19V}r)uHe0f3=O~)d=P(1w>iX{ zGbZ(VpTlSJV&wblOl6;MzKDHm#zJ43#f{1f4)cvH1 zJaYOa%Z-_Q#6b_u!UJ;w7xC9|Oh`Gmgv<94r1l)ksEsZ|C?FjY1M}c$VZ?b#mO<`= zrU)5u;Qz33*g!`s0LE48NX_CUX436u2v9Tsg>i>fC;)njRVTjKp0~bXfgBbkliN$y za3KXh;G4Qa5*PJKQ!=CDjL9|yQj3BD?Jq4ioT{%G}W zbJS~Rr?ak*m2O0mFk)QRxGT{^m0c$UB_Sv3A*{~!=uR$SirZtz;g1tudvmBGXov@9= zs>S0Ivv~E2qe2sGoksNd_EDq#(%R`Ro1_)vr56QSVA*stujsWiB5z9z+_19QO^ z;~hHW+K=3;uS0qF0|Ic3F6o2W$8oOHk?`*vZ}a#VrTRskpWC=9vdgaQUP`#~ma4^t zHtL7!L|!1(^hb||a8wA{mHCHp6rHjExJLk`&&`$gsI-)LLNmiKzA)0^X2iI3cDD$& zZcOW9*r$g;uH=>s5DrBvf7p9kwtMQ?9_BvsD5BV9aTGsHt9c$ur!>Nhq*w2F1kiS{ zGq=1{o+a;y4YhqRajbsbcwvBh#d~uvi-D^p1FC4BTffKs=!OQ6xpigqBVdE^==>}9 zsVh{KA39C(WiL?(jGATr91x`VC;JCABnWUpmvfL3sHep{o@if*4(>fogOm5Ev?;Vt z*L-#x_Qdc!ueK6$0G%Qi3f7rdSLpQQx&WO=;L$KOrm4*{VjCt%SShrx2tRb24OJ)L z<!aWuNzOv{e0HTeZ!g4nS?E0!}f6Q zw8`iFgj)pPMJHbwX+B~E&eaqJkkjdePPI!!>_mF!=H$rNzwuhGG1i_?CT2Z`6x+KZ z%{dP8Eo|?h+u8~*A?edpYFs{k0xU}=wc9K=v2(Gql+jWSp;O5hRl(pqZJ_)_ssfIF zFyWMpyc_f+p3?d87M#pjTHgK#5%<5*IUq@Z{jSU52FF_huC8R!06!_HY0YmM|4oaB z{x2Jd3-fuDRQA#O_Y{+n{BHDDk`@7gizeSW1F{|5hkVAN26Nz1og?e~Me9f7V{)!G z;k(_glcSzDf4BU~Z6^33uLf%4iFN}=jDkWKFwhV9T5Kx9=B3gaO1~piF`uWF@}N~i z`uNq8djFjc)4bTlQyTRFl->$25vSvn6^oswb)d7BiRnBse2$@kNz{sbNyX!TvZK;L z(fLek#8b@4mF|5L3dO_3eIIEmIVWq&_nHSxPZFa7(AOyEdtaLgQ7LAgH%usLIzGF5 z=ZO6U;k3xFo8K>d1Fk3`7r@qTvsBm)-cSX=6x-bKFI$C%EnpEJ?B1bt@1<*Cz5}ay z>(FW)Txo0UXW(}@Rx>~&g&C1>g!(Z`g<#Eu{$nl{nwHEx7Qf1)3? zvO^rsdXo+S;AyXK)VISr-d7jKYfsIo^YkXWh;V6yA5ZT%{B$5=Dj!5pj~+&>xuE55 zos+Hu!w>s`t19D!wDv%fkD5dPOCfG2=M-=(EJ(!fcJtrXvev~Y#xJf$x@j`O-Es8r z3XRHeIg=8e&La)qfxv-4izm%N5|hctWvW-Yi08rtW;@54>x2e^;> zsRe}FcR{dkXObLSwOELqaJWf(Tl1c4G8S!?S$+SE3BgZ10a*WRo-eGwD94~7!~Pk? zp0ckSYk-^;F(75+=VFcV8zgGJz`VQ+s>zKoi|OKXYm`A%N%y3?q~9v0baU}j3k6{8^2iF zvqBOqBP^N|y{*>}d?6m89tC??#UT&3Ln;E}A@gqa;({x)cfvseda90mWwwz8dEOFK zbu;h1+9L8Kx}dlb1ir;0qOP4@0A0sqER_GQdF;AFr=hIIg<3d2VTPO|net8v8i+OC%>)rOz;#(DQvTZ&37S)W2oYt?hmvc7C>Mwy>z4-Z_vhs@A*Ej`uuyrdeEZY6D~kx6w9^(M3mPW5=)RA2 z9UMMWFp2mfJPin=w?O9XkWKjulMaxLg9wVg1ydCSfm8)yq+hG| zC<5^MX!qAw0+Qf)mQU`u&KnzqOo&8Cvkpd&IK}@JQ;x0_njrgFE#V&y+a%aak(r5o z$RXOHC_tM4l*mmI+MAwf8BG7^Pp$q95MYE9%ta**ufQ`<27aSmE<^2IMt&+zw^Bd_$?M_|ncsjegH{MMo1 zHbc>l)GM(wldG?pYO_lc7ukI2eBsu0P#Q;?H{m3t2h$xUEMoV))ccscPgc9#HoVnn zN)7g;zY@S>eDe)wH@jP~0%{}V=Dk;sKiZdKXSP-^cDafCUK2EtdGGySM>=Ycf<=v1 z2G;*K$YQr1bQv!QR>yt*B4PRn;7I_2bvglX3e4mSgJn7Y5k@%e$5jD3!}uq8t@KB= zXEjm@Ihbt_?tPHY2p{YEDI#*j0(ucrJUJ)H-EHiUcH`^?TacI=(}dXFL}ZlVQP#<- z@5q}pKQW}n7Na~-`%r81b>iy(=FNsC2hlw@Usn5Nz3jdDaTTblcp%T3ZtS_5Q(ryP zk)RqtK!70|S7zTtc+8nOA}(wzK&l_PPrGU;cmsYl=q`;@#JvAu4Dj{`AfNm>8?(pA zJ=$A_D*!DIl8{%cZ+s+4MIQ%!4LeWsQJt`#^<9%9bldUJ#X)C14!os_7?S?6S4{UKCBcNKyuuEIf|}* zb5%^>cT4X-LD8|YFM(G@{Elg#CbZIf03bZm>i~zEzsg(TTCqQ(15lkQ5^VX)EEGuC zgMWxjFHrm;XYN7oc;qJ0t$>E|pV&^3?|UZP zbb_d;C40)G!BgP2t)84VlZV46Uc=>QEJBupS-*JNr%(R09AAt}}`MH#z!&|4llo>DakhQ?^h| z*ldRm-k=IB9;bW2%UI&~LKduQV{9!Yxi0W23NeJ)E2+GoAzbsY!T8=VvmqC)Qq^Ti z3v$sEu;Fq5m}Y*ar!tT(Xca}R{U_C5YQy!}dpA`a(&paUl9jVv1wr7 zcl&u=r<%|r5*+})10(_{#@q2{P?f%KbR*3HRD|LjTDMCNMXu{NS3%g}+q@mQCb)>T znOv31OmeCTQqN~CPyy{|E}LngF2{pBH5{ePp19+}dy5$m~<`$2fYVTt>JR z_NGF4`KU(`XHX``GW$p$Rxyr$xYQyZ-xh2Xw^*HhkRWY^lI-xM%1~g?IJ@;}_=|k9 zThQ)p`{2w60*m>UPh6Gqd;+eZMt0NZKGB$glT`~nkn_0nH|Oz>dqF~Z)6-uNjgU_? z3bPsp2K;k>_0WK=`*zo2vP47@)O3M$A?Js3YYvYZSdkeE$t5$6$v+QH=L?gRP0#}{ z=%xNHAlZ-U@;kTz9}~Qyq6B|uLCh{3X>&s+2tSMiTX23)x;dU>T>NHD{V$F;7Nj!k z-M32%yZ2V>$ZgKW;qk@{^y7G*CIF7p0N^+s0niBlpEx95Xps+Kqyzp~4#A-%ls=$` z_hJNJi%{iLNO06v3H;sqHw&Pnj(3mY6+@qyw5purA$!@EgZQi(CU3e4Ld2RZ%H3hN z=Vf#XFsJi5Lq3rYKFz4H|7SO-fv;RKda{055>@?@eyP~w_d9-7qqTOgw>!S-lYz$S zj`3jKo~O;pE9Q8v1UGLpU1aP!zPxMoQ}^g3kM#1%LhRzGATy8wbtK3x0%XZ!W~$Yt zhStc8zTLbn1N&;S`$`TMS%25`^c&(!Hef2ao>ESk^7sB`pC&8|h+?+FLz5tKBl2@# zdulY7^{87mnley%N8|34{R7nXN`jx14gcWO301M&40y?zK>XOnB{M!-MhM>8`!=5J zIGd%5RgGKyV0_XSZ&IL_9f@!H#TfEA9eCgYv~S0;nq}DB^XsvBlO!W8n54ji*VPV@<+RedN1X7b}4D)WK=Ny27$Oy373A&d=U~ zS%0VGOzY197RSUdX0d-w;Qz|=0Og%&4{kM|RlG~V6f#d*=oB#S>_B>Xz{*_^$6iM8 zD<70*zS0ji*!ibqeCkhIwJB2N-*y|hI3T?zZi&W?nFix&@ zU8t4fMGr+?8zcY;jY$dvs{E#2$74X%C}B*8>`RBz4G|AKXkj5nlnVZN832Fg3jRS{ z@B5l_NyTT%S9+2$E$v2sZNU1EU=Qakda<>Q-9pgArrCWlmR(-z{&4Vt=4GDKox9BS zN@T6|X`DKExl1N5@A4fbU4?Ens-Qg-d*zF+1jUqUuiRa^TLGDq>p>S(EO=Ax{$_{! z$9s+jHR0!To-eJwuR8x(Y4#QKJ1RN1KX-vVNc6<6TW(j2SwflfPn@87932?d^EI?p@Wjp3}AfmVI;PF(=_;3>RlM79n4p?ye zK%8#h=u#O82<`JR{RQe?d!yI+J4?-Dey%Ux-Au58g;rSm7JlNo0+K?r%Hnfbx$d;0zWAP!zTtY!1y789*rJ*aJ;rGmB=GM+M$c7Y6C z`=~b9pKbVSL1h~-SBv~7bG6nip&RX8+p0Mf%z=peUR`J<{;@8RqGKGs2gj+Y1 zqrde8v}j1T(JAx!+$f>HcSq^L#c#LHa$gKZE79De*5SOdeC>^r(pS|Dd#gBttHAe8 zlSy$AnfXGb-pJ%=x>r25aBban@%tr0>Ms-X(I+EA4-S8;!KGk^BN%+m@fC*VUd#w0 zG5$bxSP({6m^$(GlxJhpX^L|#Uwp8pvxwS&@^mqfZ7Nt&!h}R(Ba00?54>HKbCMubH%-zxwIJk zZRbXv0R;3m&LR-HTiTdg!=dMp<1M$}4xmd@XuTi5?Izn84xPtMj1rq??dMenHg!uL z$QWzr4P)Egv5gW4&+QWXV4KX%W&tc*VSaBjavcX>DDCCX5YzAo505-MK6G&vtC?u4 zsqn!LKV-10lxQ1(``~Dk<$`$XjK`#~Au({6v1b&&MW_%~wSnpk=aX|=3hGteDJG>! zL3n($k*T+}!~B4LH&VZ8IJ^6WU4A&adh4$3PbD4$igO=Lo;RRb*$+lN!=o9!hwHFQ zH}E<0gt8THiS^Y@UsUI6&5}l2^vmNQ%IwYH>@7&`RKDd4UO%JN%?vljT;7k5p^YjvpRtIQf_>5}2C;;IcC&A3UkTc1#Z`%0rpjvws2b(RWp zpNP-l;0h*KxZb4FwuH(ZR1T_@cM-PB-tOW)mhU!o_5SCwBP54M46Y)#g0?VxSJ=k> z45yExI~m$vZ5yJFG(hw8uV|##wCxw?8nS)>M#QnKokN0y%6%d@ zM!7acQ)s6gBCBkRzE+K{))GEPdJ;D84DTm(;n$U8hGeO;YKlgn3cz+a`)d>R3->`h za0PkkDfYpKpUN>*<0=Dx*QyS6(3sQ=FLzuyvIxw=9_2qNxpDHV1a6b-p7`f^(BAQ- z7w|`zqnuT{oV(r%X?eF9FjIP;;iF2iTIi`E9TZYUdHndN=78WHuyJM*6}m`&LyZwn zYMg3~%53NgzZs|Zh|`@@B=L1TRYk|Lx!}!>ao>f8y!8%Ww=vX|an#IY3XJ2%&FK19 z_hkeXYbQbOY@Ojem+6z!ynkT@Q0m8Z$IxuKWeAq`1ZjIEDBPL|M6@Wk`av( zflmo9`RLwv-ewRR_s+?7ph6tsZjG3eowW!oVJd0NsM*xJtfkp9$-d8kP{X9wDaA~B ztQ$!>d=wquJeuue^C_ zgine+SPlD-R_VPTdFc8}Q1@j6VjA`2_vUA42U52T3;mc6N6ZQ~Jl(4mT=ieB9tLw5sRbZ9ba;txEG57hED@`jC}cs(6dMsog&Yhdx>!?q=bueiQP2>f1g zb$(~?)Krg?*UkhYGooSCMtlwTLGpUC0b}^!7qKK13wmFNve0JerXuyz=wWaX zwaAn?6srSq98Y&X5gz2e)Ydyp!sR8OOjZWHurJ`V@&5{C|0*S#bf3g`iy}Iwz`P`E zA^t2f@QmS*ar`oLy|DS06^iiSj`+nAhQTEp!`^nApGEHn1wUuMJf3VS#yv_j>J2;* zhiN+~X+2HZ>JeOZovoe*P7y>OSeG(EWN@4WDz>z`41w@0$5gf*en@p>23A8qiST6g z$U88Zz?R)UaMZnG2*oo2A0aq>$ly7IZC9CB!n0c(c^KSab@WDY2%bh|+ixGR>NO(Q zef12H(te}VMj3`jvUf;GDca6&$>)4h$oClBmd}}shvr%7yQ33jtoE`Z259bYh79xs zu#!@&@bqBJ4e=FpMONmIm%HB(U`oTK6NAuKRO6q1ZI`-Zz0taaply@gcE-qmD(Ut? z;Osuf(Y!~~CM;MRuk;wcMLxmQ77>N2iQZcEz&6feOR+lXZZXijSR8!xH*S(W@}t5R z3_i-^3u>IWAsJ&Ws{R(7lK0xduRf%Q4+wta9}w`!&qC-QYI!^=c`?Y>Dv&gLh3pK) zjcM;E?h>0Knccu9xyW~mm&adUcCY0U45NQNfHmlfsrv7#^CD!OY-aP0W}V7@G{YE8B3v`fc=Dnd{~Lq7gp7O+L~j~=m1&GmU=7Ci3hlWB z+sJ;o%|6aPcvd4hYf4Yy1E+`Vjc*vpb=I@v9^ckitrKlqFnD+s^Ko89<1B2A2ac+N z<`_=?zJE0j`mvp^W2)z>BEm_pV9)}Ob$2+B%VLOQTsgwG@R|++g=NXRlUy&d;987a zhfS^9dlIslGY1?`O;(lT18t;tfntly(M-kO%gMaF`rs(8=%+qw9_sSyBQ8k_vs;`z z3dc&1?`BKpAWFR@cSVA(%FI9T@%tGTZ;z1iIR3H4wI$&mjQ`mQ>WRS9RWjOyB`+fY$@uv!Y*|ls!^or`c z58_nSdxV51Ls-@Xc%m(|GdyE+7?Xult`q~FIh~NdaQige9!F zIvXb;P=cr^rnfpxk5HB!OB3cTOY_+gR#>q!C1620PgT^(x^jQQ;^pKd{>W)@!?C0H z-Q86mzw^L>q&Q=X7%cpOL|9GaPxNV?=nYZaQ~UV+TvSYerf>_KLjGCB!1CgC@vFcu z0i)GFWIVO^xE* zLzIt8r@nC8(`y|iKV;A^@soSqELZQ5_%@uG8+dnNwU)B*;xjJ7!zg-@Y>H2cxKp;~ zc5xA!22JimHv->F@zb|Sp8NQ$R>#Am!Psx;M=#o{k}}tuYuEv z+Q~TP%lX zt&)YS@*Uu(oRYrIU6I{I{gZ^=s+mVyFU8U-`#~@Nj;#@|sV%X%gcezKld^Kb|+q`48ME3|X zek&K)9Jo^xPh;7}V|etxRHx3ohb|-9g_S5Sa8Flw0L?nDX@nDlz{=QQY<}H&0kShG zhP5TRxi8++rJH`!MY|h0EFeW6UpH6eRPimrGM-3?S+zu57dLeG94-fMDTfOoyYBof zH{u3)@WO!?t?(=@0YJ-`KucEgQ{LF${_gUQj@8Pp;iwFMxL{zt(z4I}^NO6#WCpA0 zJgA>K4-8IQemfDRbTY6EbKneNCtatF6+(^`*k;o8iM+V~mW0gue8`~Sc!~vIuzZD$ z2`m1oK~udX@YQ*dmakd8iZCy=m-YU5t(}7o8pIB6k2Z?Wv$RoE$g%oZi9pfeGr5l= z`hdZr#z)-HTU!lQreZgB32rrmO(=GMcBxwLK(3b@c<4K|&OdHiGy6;U8TC&SLf?Of z#aMrT(&Vf7iGt5&TCT&Gve2;;(iY(@H35Ez8>MIv`*_iZ=wy>mdd~)x%_ok%4J3|B zD#iwsNPHhtG8Z2V9*6b!2`?CRjSk)DiVFL)A0O6s#Pplsm=~(-7#%dLE~?4gy~C+Y z_7gW>C>#r%qDX~-y`7t$WmI$53X-)%m8pTX{dL~0N9Nnr*tFH2tdjY{*dHR!A?d0V z=a`>8x6Taz3=GY7&G2{*{OO1}9bhAb)n>gvT#?4GzbU=1yEnn3mne5-$2PIt2V#idj6RryC>4vJ_@F5hYvgpFdeZIyvDyK>`sM`fzVrb-A; zHszPKZRV}D=cz2eoxz=cku(=b6V78%8OOYQv;5Wng^ro$aN9kYGuJ=+9qTz=-kwhD zUC7R%X}Tw6R)$s|Gj>MD3P~pbl}0f^g|vLaxb?kzr+=gaI^5=VaL!M`EN};!Qi_Vw zTJP-(D=_LpG)p{36^4(Nz6n;+0byXX$!n1{z^C6K`%*4;r#FJS_|7Vu#Uqzi$BT-< zADqOPUvx{sbf|*25`Alha~b%O&fT~kQrPiDi<^P6P(s&k!f@^k^MwU1`tH>;FPiB}>ff9c;PXZLq*h;cn>M_1SHKFt zU#qr#3%Ib)eepTQmDZcS`pTxh+FR8%P$M$WKdUHu@ykfkPrqEZ z^;f2^DJKAZNs=DQ!O~$_%QOL0UOqlDyBjp*RUN9p|Kay_=`UD=S+h9Sm%S?qec_H5 z4GJg#KEZ=xu5$jT2%7M_4|w7=Qq+Mp?2*@G8VyU>3LkT>LINCaztzS_-p& zGa2<}iHwv@NAM|5nZ$SG%!G4qzm4E0HBnLuu4ci_O#K>WpKEHsZ)PUT)3jI@61di3 zC2%~Ev@3xqFn()&_A2MtJzyigVr*HKOATs%WzWM+^H`TYwm;nIol)v^N7Va!%m5a~ zLptn?%+wxEbmol=8w;a{e`r#z2Y#OHlR9WPFRJ-4Dm6cJvJck!{b%zx?NDJ5VcH`b zGU;2?NZORBVmD5oz199o8qkR61kT*<7bbIHigKD6fKD=DIe7z+&U067TABVAS#KQ| z^|Sqt%L)o2tr8MTE8QKdA}Jt9NQ-orbPIw($I_ulNOzZ@)KUV1ORBJRNlO3bweNdB z_kQp1ALJpkGiT16^E^4TC_2th5A>jeP{zrb?mOV~{oJeMJ{xlp6&sF4y&nYDB{FV< z%PY6f%x4cQ&X}m0aQ4WC@zjYjJ&&U{XLd%;{hRyQK$0>jXnavQSARx`$bEa8tO*sB z-1RRGH{veDkoEej%)LG2Q?V%{*EQoAkSBm}E6CBl{2;cqtn|sn`xk4CgEwm};`zcq zsN~>(G0yh?x>GyG-93o*k!7Cf{T&7@;TLU$&gX<0Sjj}ucby5%MFG9mAMP@Gn=U`4V=@kdrdAB~ z%hWT=k28Lpo*s-G$eY%vH$fjIg&hr^p1Mx1)UrSiEw~TzNt?;iTm>vRoDrB6oKJ~ z@(vH{&GPrp9Pr&XORO=FINe^s0l2knf(ZB_S%0A$f7JISNyM-;qJb&p8-*|oUyC_L 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 ak6EEUbWN|Ac86)epPsh Date: Sat, 7 Jan 2023 13:12:41 +0000 Subject: [PATCH 139/141] Update all development Yarn dependencies (2023-01-07) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a719ed8118..24710f3deb 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "prettier": "^2.8.1", "process": "^0.11.10", "puppeteer": "^19.4.1", - "sass": "^1.56.2", + "sass": "^1.57.1", "sass-loader": "^13.2.0", "stream-browserify": "^3.0.0", "swc-loader": "^0.2.3", diff --git a/yarn.lock b/yarn.lock index ec054b8a5f..c5672739b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8323,10 +8323,10 @@ sass-loader@^13.2.0: klona "^2.0.4" neo-async "^2.6.2" -sass@^1.56.2: - version "1.56.2" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.2.tgz#9433b345ab3872996c82a53a58c014fd244fd095" - integrity sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w== +sass@^1.57.1: + version "1.57.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.57.1.tgz#dfafd46eb3ab94817145e8825208ecf7281119b5" + integrity sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" From 9914fff2a953a12c867f87f99c3ad2dff91c130c Mon Sep 17 00:00:00 2001 From: Leonardo Date: Thu, 5 Jan 2023 02:05:37 +0000 Subject: [PATCH 140/141] Translated using Weblate (Portuguese (Brazil)) Currently translated at 76.9% (348 of 452 strings) Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/pt_BR/ --- src/i18n/locales/pt_BR/translation.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/i18n/locales/pt_BR/translation.json b/src/i18n/locales/pt_BR/translation.json index 0a7212e959..3aae9cd8cc 100644 --- a/src/i18n/locales/pt_BR/translation.json +++ b/src/i18n/locales/pt_BR/translation.json @@ -598,7 +598,7 @@ "success": "Sucesso", "error": "Erro", "settings": "Configurações", - "websites": "Sites", + "websites": "Atividade", "sats_one": "sat", "sats_other": "sats", "loading": "carregando", @@ -636,7 +636,8 @@ "help": "Ajuda", "response": "Resposta", "success_message": "{{amount}}{{fiatAmount}} enviados para {{destination}}", - "advanced": "Avançado" + "advanced": "Avançado", + "discover": "Explorar" }, "components": { "allowance_menu": { From 8517e13a3fe4c583e91d7a02c6dcd7413d3a1059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?BSN=20=E2=88=9E/21M=20=E2=82=BF?= Date: Sat, 7 Jan 2023 13:17:20 +0000 Subject: [PATCH 141/141] Translated using Weblate (German) Currently translated at 69.6% (321 of 461 strings) Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/de/ --- src/i18n/locales/de/translation.json | 68 +++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 31945745ef..d092f0eb45 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -4,7 +4,7 @@ "nav": { "welcome": "Willkommen", "password": "Dein Passwort", - "connect": "Dein Lightning-Konto", + "connect": "Ihr Lightning Konto", "done": "Fertig" }, "intro": { @@ -276,7 +276,12 @@ "errors": { "connection_failed": "Verbindung fehlgeschlagen. Ist dein Core Lightning-Knoten online und verwendet es das Commando-Plugin?" } - } + }, + "kollider": { + "title": "Kollider", + "description": "Anmeldung bei deinem Kollider-Konto" + }, + "title": "Verbinde Lightning Wallet" }, "unlock": { "unlock_error": { @@ -584,6 +589,65 @@ "heading": "Einen Kanal vom Knoten anfordern" }, "success": "Kanalanfrage erfolgreich gesendet" + }, + "alby": { + "pre_connect": { + "email": { + "login": { + "label": "Email Adresse oder Lightning Adresse" + }, + "create": { + "label": "Email Adresse" + } + }, + "login_account": "Melden dich an, um dein bestehendes Alby-Konto zu verbinden.", + "host_wallet": "Wir hosten die Lightning Wallet für dich!", + "optional_lightning_note": { + "part3": ". Dies ist eine einfache Möglichkeit für jeden, dir Bitcoin über das Lightning-Netzwerk zu senden.", + "part1": "Ihr Alby-Konto enthält außerdem ein optionales", + "part2": "Lightning Adresse", + "part4": "mehr erfahren" + }, + "create_account": "Erstelle ein neues Alby-Konto, um Bitcoin-Zahlungen zu senden und zu empfangen.", + "optional_lightning_address": { + "suffix": "@getalby.com", + "label": "Wähle deine Lightning-Adresse (optional)", + "title": "Zahlen und Buchstaben, mindestens 3 Stellen" + }, + "errors": { + "create_wallet_error": "Du konntest dich nicht anmelden oder ein neues Konto erstellen. Wenn du Hilfe benötigst, wende dich bitte an den support@getalby.com" + }, + "forgot_password": "Passwort vergessen?", + "title": "Dein Alby Konto", + "set_password": { + "choose_password": { + "label": "Password" + }, + "confirm_password": { + "label": "Passwort bestätigen" + }, + "errors": { + "enter_password": "Bitte gebe dein Passwort ein.", + "confirm_password": "Bestätige dein Passwort", + "mismatched_password": "Die Passwörter stimmen nicht überein." + } + } + } + }, + "choose_path": { + "other": { + "and_more": "& mehr...", + "title": "Andere Wallets", + "description": "Verbinde dich mit deiner bestehenden Lightning Wallet oder Node und wählen aus verschiedenen Anschlüssen.", + "connect": "Verbinden" + }, + "alby": { + "title": "Alby", + "description": "Melden dich an oder nutze dein bestehendes Alby-Konto, um im Handumdrehen eine Lightning Zahlung zu beginnen.", + "create_new": "Anmelden" + }, + "title": "Verbinden", + "description": "Um Alby für Online-Zahlungen zu nutzen, verbinde deine Lightning Wallet mit der Erweiterung." } }, "components": {