From a87ce1cd5e1aab0a063f1a6ecb1760583fd16786 Mon Sep 17 00:00:00 2001 From: Agustin Kassis Date: Wed, 15 May 2024 05:46:47 -0300 Subject: [PATCH] feat: add lawallet extension support (#2934) * Adding LaWallet.io extension. * Full support with NIP-05 and LUD-16. * Added nostr-tools as lawallet stack requires nostr websocket connection. --------- Co-authored-by: Jonathan Llamas --- package.json | 1 + src/app/router/connectorRoutes.tsx | 15 + .../ConnectLaWallet/LaWalletToast.tsx | 44 ++ .../connectors/ConnectLaWallet/index.tsx | 206 +++++++ .../background-script/connectors/index.ts | 2 + .../background-script/connectors/lawallet.ts | 531 ++++++++++++++++++ src/extension/providers/nostr/types.ts | 2 + src/i18n/locales/en/translation.json | 24 + src/i18n/locales/es/translation.json | 24 + static/assets/icons/lawallet.png | Bin 0 -> 19877 bytes 10 files changed, 849 insertions(+) create mode 100644 src/app/screens/connectors/ConnectLaWallet/LaWalletToast.tsx create mode 100644 src/app/screens/connectors/ConnectLaWallet/index.tsx create mode 100644 src/extension/background-script/connectors/lawallet.ts create mode 100644 static/assets/icons/lawallet.png diff --git a/package.json b/package.json index 5c0649a965..e7e052f9b3 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "lodash.merge": "^4.6.2", "lodash.pick": "^4.4.0", "lodash.snakecase": "^4.1.1", + "nostr-tools": "^1.17.0", "pubsub-js": "^1.9.4", "react": "^18.2.0", "react-confetti": "^6.1.0", diff --git a/src/app/router/connectorRoutes.tsx b/src/app/router/connectorRoutes.tsx index 4cd32fdb20..2ea902deb9 100644 --- a/src/app/router/connectorRoutes.tsx +++ b/src/app/router/connectorRoutes.tsx @@ -3,6 +3,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 ConnectLaWallet from "@screens/connectors/ConnectLaWallet"; import ConnectLnbits from "@screens/connectors/ConnectLnbits"; import ConnectLnc from "@screens/connectors/ConnectLnc"; import ConnectLnd from "@screens/connectors/ConnectLnd"; @@ -23,6 +24,7 @@ import core_ln from "/static/assets/icons/core_ln.svg"; import eclair from "/static/assets/icons/eclair.jpg"; import galoyBitcoinJungle from "/static/assets/icons/galoy_bitcoin_jungle.png"; import galoyBlink from "/static/assets/icons/galoy_blink.png"; +import lawallet from "/static/assets/icons/lawallet.png"; import lightning_node from "/static/assets/icons/lightning_node.png"; import lightning_terminal from "/static/assets/icons/lightning_terminal.png"; import lnbits from "/static/assets/icons/lnbits.png"; @@ -165,6 +167,12 @@ const connectorMap: { [key: string]: ConnectorRoute } = { title: i18n.t("translation:choose_connector.nwc.title"), logo: nwc, }, + lawallet: { + path: "lawallet", + element: , + title: i18n.t("translation:choose_connector.lawallet.title"), + logo: lawallet, + }, }; function getDistribution(key: string): ConnectorRoute { @@ -252,6 +260,13 @@ function getConnectorRoutes(): ConnectorRoute[] { connectorMap["voltage"], connectorMap[galoyPaths.blink], connectorMap[galoyPaths.bitcoinJungle], + getDistribution("citadel"), + getDistribution("umbrel"), + getDistribution("mynode"), + getDistribution("startos"), + getDistribution("raspiblitz"), + connectorMap["nwc"], + connectorMap["lawallet"], connectorMap["lnd-hub-go"], connectorMap["eclair"], ]; diff --git a/src/app/screens/connectors/ConnectLaWallet/LaWalletToast.tsx b/src/app/screens/connectors/ConnectLaWallet/LaWalletToast.tsx new file mode 100644 index 0000000000..2d12977171 --- /dev/null +++ b/src/app/screens/connectors/ConnectLaWallet/LaWalletToast.tsx @@ -0,0 +1,44 @@ +import { Trans, useTranslation } from "react-i18next"; + +export default function LaWalletToast({ domain }: { domain: string }) { + const { t } = useTranslation("translation", { + keyPrefix: "choose_connector.lawallet.errors.toast", + }); + + return ( + <> +

+ {t("title")} ( + {t("message", { domain })}) +

+ +

{t("verify")}

+
    +
  • {t("match")}
  • +
  • + ]} + /> +
  • +
  • + ]} + /> +
  • +
  • + ]} + /> +
  • +
+ + ); +} diff --git a/src/app/screens/connectors/ConnectLaWallet/index.tsx b/src/app/screens/connectors/ConnectLaWallet/index.tsx new file mode 100644 index 0000000000..36ed8f6a35 --- /dev/null +++ b/src/app/screens/connectors/ConnectLaWallet/index.tsx @@ -0,0 +1,206 @@ +import ConnectorForm from "@components/ConnectorForm"; +import TextField from "@components/form/TextField"; +import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import toast from "~/app/components/Toast"; +import msg from "~/common/lib/msg"; + +import Button from "~/app/components/Button"; +import PasswordViewAdornment from "~/app/components/PasswordViewAdornment"; +import LaWalletToast from "~/app/screens/connectors/ConnectLaWallet/LaWalletToast"; +import LaWallet, { + HttpError, +} from "~/extension/background-script/connectors/lawallet"; +import Nostr from "~/extension/background-script/nostr"; +import { ConnectorType } from "~/types"; +import logo from "/static/assets/icons/lawallet.png"; + +const initialFormData = { + private_key: "", + api_endpoint: "https://api.lawallet.ar", + identity_endpoint: "https://lawallet.ar", + ledger_public_key: + "bd9b0b60d5cd2a9df282fc504e88334995e6fac8b148fa89e0f8c09e2a570a84", + urlx_public_key: + "e17feb5f2cf83546bcf7fd9c8237b05275be958bd521543c2285ffc6c2d654b3", + relay_url: "wss://relay.lawallet.ar", +}; + +export default function ConnectLaWallet() { + const navigate = useNavigate(); + const { t } = useTranslation("translation", { + keyPrefix: "choose_connector.lawallet", + }); + const [passwordViewVisible, setPasswordViewVisible] = useState(false); + const { t: tCommon } = useTranslation("common"); + const [formData, setFormData] = useState(initialFormData); + const [loading, setLoading] = useState(false); + const [showAdvanced, setShowAdvanced] = useState(false); + + function getConnectorType(): ConnectorType { + return "lawallet"; + } + + 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 { + private_key, + api_endpoint, + identity_endpoint, + ledger_public_key, + urlx_public_key, + relay_url, + } = formData; + + const publicKey = new Nostr(private_key).getPublicKey(); + const domain = identity_endpoint.replace(/https?:\/\//, ""); + + let username; + try { + const response = await LaWallet.request<{ username: string }>( + identity_endpoint, + "GET", + `/api/pubkey/${publicKey}`, + undefined + ); + username = response.username; + } catch (e) { + if (e instanceof HttpError && e.status === 404) { + toast.error(, { + position: "top-center", + }); + } else { + toast.error(); + } + + setLoading(false); + return; + } + + const account = { + name: `${username}@${domain}`, + config: { + privateKey: private_key, + apiEndpoint: api_endpoint, + identityEndpoint: identity_endpoint, + ledgerPublicKey: ledger_public_key, + urlxPublicKey: urlx_public_key, + relayUrl: relay_url, + }, + connector: getConnectorType(), + }; + + await msg.request("validateAccount", account); + + try { + const addResult = await msg.request("addAccount", account); + if (addResult.accountId) { + await msg.request("selectAccount", { + id: addResult.accountId, + }); + navigate("/test-connection"); + } + } catch (e) { + console.error(e); + let message = ""; + if (e instanceof Error) { + message += `${e.message}`; + } + toast.error(); + } + setLoading(false); + } + + return ( + +
+ { + setPasswordViewVisible(passwordView); + }} + /> + } + /> + + + +
+ {showAdvanced && ( +
+ + + + + +
+ )} +
+ ); +} diff --git a/src/extension/background-script/connectors/index.ts b/src/extension/background-script/connectors/index.ts index 95bbaef680..0a682d66b6 100644 --- a/src/extension/background-script/connectors/index.ts +++ b/src/extension/background-script/connectors/index.ts @@ -4,6 +4,7 @@ import Commando from "./commando"; import Eclair from "./eclair"; import Galoy from "./galoy"; import Kollider from "./kollider"; +import LaWallet from "./lawallet"; import LnBits from "./lnbits"; import Lnc from "./lnc"; import Lnd from "./lnd"; @@ -38,6 +39,7 @@ const connectors = { commando: Commando, alby: Alby, nwc: NWC, + lawallet: LaWallet, }; export default connectors; diff --git a/src/extension/background-script/connectors/lawallet.ts b/src/extension/background-script/connectors/lawallet.ts new file mode 100644 index 0000000000..999059686a --- /dev/null +++ b/src/extension/background-script/connectors/lawallet.ts @@ -0,0 +1,531 @@ +import { schnorr } from "@noble/curves/secp256k1"; +import * as secp256k1 from "@noble/secp256k1"; +import type { ResponseType } from "axios"; +import { Method } from "axios"; +import lightningPayReq from "bolt11-signet"; +import Hex from "crypto-js/enc-hex"; +import sha256 from "crypto-js/sha256"; +import { nip04, relayInit, type Relay } from "nostr-tools"; +import { Event, EventKind } from "~/extension/providers/nostr/types"; +import { Account } from "~/types"; + +import toast from "~/app/components/Toast"; +import { getEventHash } from "~/extension/background-script/actions/nostr/helpers"; +import Nostr from "~/extension/background-script/nostr"; +import Connector, { + CheckPaymentArgs, + CheckPaymentResponse, + ConnectPeerResponse, + ConnectorTransaction, + GetBalanceResponse, + GetInfoResponse, + GetTransactionsResponse, + KeysendArgs, + MakeInvoiceArgs, + MakeInvoiceResponse, + SendPaymentArgs, + SendPaymentResponse, + SignMessageArgs, + SignMessageResponse, +} from "./connector.interface"; + +interface Config { + privateKey: string; + apiEndpoint: string; + identityEndpoint: string; + ledgerPublicKey: string; + urlxPublicKey: string; + relayList: string[]; + relayUrl: string; +} + +export class HttpError extends Error { + status: number; + error: Error | undefined; + + constructor(status: number, message: string, error?: Error) { + super(message); + this.status = status; + this.error = error; + } +} + +export default class LaWallet implements Connector { + account: Account; + config: Config; + relay: Relay; + public_key: string; + access_token?: string; + access_token_created?: number; + refresh_token?: string; + refresh_token_created?: number; + noRetry?: boolean; + + invoices_paid: InvoiceCache = {}; + last_invoice_check: number = 0; + + constructor(account: Account, config: Config) { + this.account = account; + this.config = config; + this.public_key = new Nostr(config.privateKey).getPublicKey(); + this.relay = relayInit(config.relayUrl); + } + + async init() { + return Promise.resolve(); + } + + unload() { + this.relay.close(); + return Promise.resolve(); + } + + get supportedMethods() { + return [ + "getInfo", + "makeInvoice", + "sendPayment", + "signMessage", + "getBalance", + "getTransactions", + ]; + } + + async connectPeer(): Promise { + console.error( + `${this.constructor.name} does not implement the getInvoices call` + ); + throw new Error("Not yet supported with the currently used account."); + } + + async getTransactions(): Promise { + const _transactions: Event[] = await LaWallet.request( + this.config.apiEndpoint, + "POST", + "/nostr/fetch", + { + authors: [this.config.ledgerPublicKey, this.config.urlxPublicKey], + kinds: [1112], + since: 0, + "#t": ["internal-transaction-ok", "inbound-transaction-start"], + "#p": [this.public_key], + } + ); + + const transactions = _transactions.map((event) => { + return { + ...event, + kind: event.kind as EventKind, + }; + }) as Event[]; + + const parsedTransactions: ConnectorTransaction[] = await Promise.all( + transactions.map( + parseTransaction.bind(this, this.public_key, this.config.privateKey) + ) + ); + + return { + data: { + transactions: parsedTransactions.sort( + (a, b) => b.settleDate - a.settleDate + ), + }, + }; + } + + async getInfo(): Promise< + GetInfoResponse<{ + alias: string; + pubkey: string; + lightning_address: string; + }> + > { + const { username, nodeAlias } = await LaWallet.request<{ + username: string; + nodeAlias?: string; + }>( + this.config.identityEndpoint, + "GET", + `/api/pubkey/${this.public_key}`, + undefined + ); + const domain = this.config.identityEndpoint.replace("https://", ""); + return { + data: { + alias: nodeAlias || domain, + pubkey: this.public_key, + lightning_address: `${username}@${domain}`, + }, + }; + } + + async getBalance(): Promise { + const filter = { + authors: [this.config.ledgerPublicKey], + kinds: [31111], + "#d": [`balance:BTC:${this.public_key}`], + }; + + const events: Event[] = await LaWallet.request( + this.config.apiEndpoint, + "POST", + "/nostr/fetch", + filter + ); + + const balanceEvent = events[0]; + + return { + data: { + balance: balanceEvent + ? parseInt( + balanceEvent?.tags.find( + (tag) => tag[0] === "amount" + )?.[1] as string + ) / 1000 + : 0, + }, + }; + } + + async sendPayment(args: SendPaymentArgs): Promise { + const paymentRequestDetails = lightningPayReq.decode(args.paymentRequest); + const unsignedEvent: Event = { + kind: 1112 as EventKind, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ["t", "internal-transaction-start"], + ["p", this.config.ledgerPublicKey], + ["p", this.config.urlxPublicKey], + ["bolt11", args.paymentRequest], + ], + content: JSON.stringify({ + tokens: { BTC: paymentRequestDetails.millisatoshis?.toString() }, + }), + }; + + const event: Event = finishEvent(unsignedEvent, this.config.privateKey); + + try { + await LaWallet.request( + this.config.apiEndpoint, + "POST", + "/nostr/publish", + event, + "blob" + ); + this.relay.connect(); + return this.getPaymentStatus(event); + } catch (e) { + console.error(e); + if (e instanceof Error) toast.error(`${e.message}`); + throw e; + } + } + + async keysend(args: KeysendArgs): Promise { + console.error( + `${this.constructor.name} does not implement the keysend call` + ); + throw new Error("Keysend not yet supported."); + } + + private onZapReceipt(event: Event) { + const pr = event.tags.find((tag) => tag[0] === "bolt11")?.[1] as string; + const paymentHash = lightningPayReq.decode(pr).tagsObject.payment_hash!; + this.invoices_paid[paymentHash] = true; + } + + private async getPaymentStatus(event: Event): Promise { + const paymentRequestDetails = lightningPayReq.decode( + event.tags.find((tag) => tag[0] === "bolt11")?.[1] as string + ); + const amountInSats = paymentRequestDetails.satoshis || 0; + const payment_route = { total_amt: amountInSats, total_fees: 0 }; + + await this.relay.connect(); + return new Promise((resolve, reject) => { + const sub = this.relay.sub([ + { + authors: [this.config.ledgerPublicKey, this.config.urlxPublicKey], + "#e": [event.id!], + "#t": [ + "internal-transaction-error", + "internal-transaction-ok", + "outbound-transaction-start", + ], + }, + ]); + + sub.on("event", async (event) => { + const tag = event.tags.find((tag) => tag[0] === "t")![1]; + const content = JSON.parse(event.content); + switch (tag) { + case "internal-transaction-ok": // Refund + if (event.tags[1][1] === this.public_key && !!content.memo) { + return reject(new Error(content.memo)); + } + break; + case "internal-transaction-error": // No funds or ledger error + return reject(new Error(content.messages[0])); + case "outbound-transaction-start": // Payment done + return resolve({ + data: { + preimage: await extractPreimage(event, this.config.privateKey), + paymentHash: paymentRequestDetails.tagsObject.payment_hash!, + route: payment_route, + }, + }); + } + }); + }); + } + + async checkPayment(args: CheckPaymentArgs): Promise { + const zapReceipts = await this.getZapReceipts(10, this.last_invoice_check); + zapReceipts.forEach(this.onZapReceipt.bind(this)); + return { + data: { + paid: !!this.invoices_paid[args.paymentHash], + }, + }; + } + + signMessage(args: SignMessageArgs): Promise { + if (!this.config.apiEndpoint || !this.config.privateKey) { + return Promise.reject(new Error("Missing config")); + } + if (!args.message) { + return Promise.reject(new Error("Invalid message")); + } + + return Promise.resolve({ + data: { + message: args.message, + signature: schnorr + .sign(sha256(args.message).toString(Hex), this.config.privateKey) + .toString(), + }, + }); + } + + async makeInvoice(args: MakeInvoiceArgs): Promise { + const unsignedZapEvent = makeZapRequest({ + profile: this.public_key, + event: null, + amount: (args.amount as number) * 1000, + comment: args.memo, + relays: this.config.relayList, + }); + + const zapEvent: Event = finishEvent( + unsignedZapEvent, + this.config.privateKey + ); + + const params = { + amount: String((args.amount as number) * 1000), + comment: args.memo, + nostr: JSON.stringify(zapEvent), + lnurl: this.public_key, + }; + + const url = `/lnurlp/${this.public_key}/callback?${new URLSearchParams( + params + )}`; + + const data = await LaWallet.request<{ + pr: string; + }>(this.config.apiEndpoint, "GET", url); + + const paymentRequestDetails = lightningPayReq.decode(data.pr); + + this.last_invoice_check = Math.floor(Date.now() / 1000); + + return { + data: { + paymentRequest: data.pr, + rHash: paymentRequestDetails.tagsObject.payment_hash!, + }, + }; + } + + private async getZapReceipts( + limit: number = 10, + since: number = 0 + ): Promise { + const filter = { + authors: [this.config.urlxPublicKey], + kinds: [9735], + since, + limit, + "#p": [this.public_key], + }; + + const zapEvents: Event[] = await LaWallet.request( + this.config.apiEndpoint, + "POST", + "/nostr/fetch", + filter + ); + + return zapEvents; + } + + // Static Methods + + /** + * + * @param url The URL to fetch data from. + * @param method HTTP Method + * @param path API path + * @param args POST arguments + * @param responseType + * @returns + * @throws {HttpError} When the response has an HTTP error status. + */ + static async request( + url: string, + method: Method, + path: string, + args: Record = {}, + responseType: ResponseType = "json" + ): Promise { + const headers = new Headers(); + headers.append("Accept", "application/json"); + headers.append("Content-Type", "application/json"); + + let body = null; + let res = null; + + if (method !== "GET") { + body = JSON.stringify(args); + } + + try { + res = await fetch(`${url}${path}`, { + headers, + method, + body, + }); + } catch (e: unknown) { + throw new HttpError(0, "Network error", e as Error); + } + + if (!res.ok) { + throw new HttpError(res.status, await res.text()); + } + return await (responseType === "json" ? res.json() : res.text()); + } +} + +interface TransactionEventContent { + tokens: { BTC: number }; + memo?: string; +} + +interface InvoiceCache { + [paymentHash: string]: boolean; // paid +} + +/** Utils Functions **/ + +async function extractPreimage( + event: Event, + privateKey: string +): Promise { + try { + const encrypted = event.tags.find( + (tag) => tag[0] === "preimage" + )?.[1] as string; + + const messageKeyHex: string = await nip04.decrypt( + privateKey, + event.pubkey as string, + encrypted + ); + + return messageKeyHex; + } catch (e) { + return ""; + } +} + +export async function parseTransaction( + userPubkey: string, + privateKey: string, + event: Event +): Promise { + const content = JSON.parse(event.content) as TransactionEventContent; + // Get bolt11 tag + const bolt11 = event.tags.find((tag) => tag[0] === "bolt11")?.[1] as string; + + let paymentHash = event.id; + let memo = content.memo || ""; + + // Check if the event is a payment request + if (bolt11) { + const paymentRequestDetails = lightningPayReq.decode(bolt11); + paymentHash = paymentRequestDetails.tagsObject.payment_hash!; + memo = paymentRequestDetails.tagsObject.description || memo; + } + + return { + id: event.id!, + preimage: await extractPreimage(event, privateKey), + settled: true, + settleDate: event.created_at * 1000, + totalAmount: content.tokens.BTC / 1000, + type: event.tags[1][1] === userPubkey ? "received" : "sent", + custom_records: {}, + memo: memo, + payment_hash: paymentHash, + }; +} + +export function makeZapRequest({ + profile, + event, + amount, + relays = ["wss://relay.lawallet.ar"], + comment = "", +}: { + profile: string; + event: string | null; + amount: number; + comment: string; + relays: string[]; +}): Event { + if (!amount) throw new Error("amount not given"); + if (!profile) throw new Error("profile not given"); + + const zr: Event = { + kind: 9734, + created_at: Math.round(Date.now() / 1000), + content: comment, + tags: [ + ["p", profile], + ["amount", String(amount)], + ["relays", ...relays], + ], + }; + + if (event) { + zr.tags.push(["e", event]); + } + + return zr; +} + +export function finishEvent(event: Event, privateKey: string): Event { + event.pubkey = new Nostr(privateKey).getPublicKey(); + event.id = getEventHash(event); + event.sig = signEvent(event, privateKey); + return event; +} + +export function signEvent(event: Event, key: string) { + const signedEvent = schnorr.sign(getEventHash(event), key); + return secp256k1.etc.bytesToHex(signedEvent); +} diff --git a/src/extension/providers/nostr/types.ts b/src/extension/providers/nostr/types.ts index 85e8f2f357..1ad1bf2a35 100644 --- a/src/extension/providers/nostr/types.ts +++ b/src/extension/providers/nostr/types.ts @@ -15,4 +15,6 @@ export enum EventKind { Contacts = 3, DM = 4, Deleted = 5, + ZapRequest = 9734, + ZapReceipt = 9735, } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 6f2d44102a..02dfef3ae2 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -268,6 +268,30 @@ "connection_failed": "Connection failed. Is the BTCPay connection URL correct and accessible?" } }, + "lawallet": { + "title": "LaWallet.io", + "page": { + "title": "Connect to LaWallet", + "instructions": "Set your LaWallet credentials below" + }, + "private_key": "Private Key", + "api_endpoint": "API Endpoint", + "identity_endpoint": "Identity Endpoint", + "ledger_public_key": "Ledger Public key", + "urlx_public_key": "URLx Public key", + "relay_url": "Relay URL", + "errors": { + "toast": { + "title": "Identity not found", + "message": "You have no lightning address in the identity \"{{domain}}\"", + "verify": "Verify the following", + "match": "Identity Endpoint must match your Lightning Domain", + "walias": "Is your lightning address USERNAME<0>@{{domain}} or its using another domain?", + "endpoint": "The correct endpoint is built from https://app.<0>{{domain}}", + "http": "The identity provider is using the same protocol (<0>HTTP or <0>HTTPS)" + } + } + }, "commando": { "title": "Core Lightning", "page": { diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 81038166c7..9b329004b7 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -178,6 +178,30 @@ "connection_failed": "La conexión falló. ¿Es correcto el URI de LNDHub?" } }, + "lawallet": { + "title": "LaWallet.io", + "page": { + "title": "Conectar a LaWallet", + "instructions": "Ingresá los datos de LaWallet.io a continuación." + }, + "private_key": "Llave privada", + "api_endpoint": "API Endpoint", + "identity_endpoint": "Identity Endpoint", + "ledger_public_key": "Ledger Public key", + "urlx_public_key": "URLx Public key", + "relay_url": "Relay URL", + "errors": { + "toast": { + "title": "No se encontró la identidad", + "message": "No hay una lightning address asignada a \"{{domain}}\"", + "verify": "Verificá lo siguiente", + "match": "El Identity Endpoint debe coincidir con el Lightning Domain", + "walias": "Tu lightning address es USERNAME<0>@{{domain}} o está utilizando otro dominio?", + "endpoint": "El Identity endpoint se deriva de esta url https://app.<0>{{domain}} de App", + "http": "El Identity endpoint utiliza el mismo protocolo (<0>HTTP or <0>HTTPS)" + } + } + }, "commando": { "pubkey": { "label": "Clave Pública" diff --git a/static/assets/icons/lawallet.png b/static/assets/icons/lawallet.png new file mode 100644 index 0000000000000000000000000000000000000000..c19899c053073707757a382199b95349b725853d GIT binary patch literal 19877 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 BL6iUh literal 0 HcmV?d00001