From 13466248004327e9a026cb89f94926f9b060d1c1 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 11 Feb 2026 17:51:24 -0800 Subject: linting --- src/app.css | 54 +- src/components/About.svelte | 3 +- src/components/Account.svelte | 53 +- src/components/Amount.svelte | 6 +- src/components/AppForm.svelte | 6 +- src/components/Avatar.svelte | 13 +- src/components/Balance.svelte | 7 +- src/components/DarkToggle.svelte | 5 +- src/components/FaqCard.svelte | 2 +- src/components/Footer.svelte | 7 +- src/components/HowItWorksCard.svelte | 6 +- src/components/ItemForm.svelte | 23 +- src/components/LandingHeader.svelte | 7 +- src/components/LandingHero.svelte | 8 +- src/components/LandingInfoCard.svelte | 2 +- src/components/Lnurlw.svelte | 5 +- src/components/LoadingSplash.svelte | 6 +- src/components/LocationsMap.svelte | 30 +- src/components/Menu.svelte | 3 +- src/components/Nostr.svelte | 9 +- src/components/Numpad.svelte | 14 +- src/components/Password.svelte | 4 +- src/components/Payments.svelte | 10 +- src/components/Pinpad.svelte | 8 +- src/components/SetAmount.svelte | 4 +- src/components/SetMemo.svelte | 4 +- src/components/SetType.svelte | 5 +- src/components/Slider.svelte | 1 - src/components/Success.svelte | 12 +- src/components/Toggle.svelte | 2 +- src/components/WalletPass.svelte | 9 +- src/hooks.server.ts | 10 +- src/lib/ark.ts | 240 ++-- src/lib/bip21.ts | 82 +- src/lib/cache.ts | 3 +- src/lib/handler.ts | 16 +- src/lib/invoice.ts | 54 +- src/lib/locales.ts | 2 +- src/lib/nip01.ts | 26 +- src/lib/nip07.ts | 79 +- src/lib/nip11.ts | 18 +- src/lib/nip17.ts | 638 ++++++----- src/lib/nip40.ts | 18 +- src/lib/nip44.ts | 298 ++--- src/lib/nip51.ts | 113 +- src/lib/nostr.ts | 360 +++--- src/lib/parse.ts | 260 ++--- src/lib/passwordCache.ts | 79 +- src/lib/punks.ts | 128 +-- src/lib/random.ts | 28 +- src/lib/rates.ts | 10 +- src/lib/samples.ts | 34 +- src/lib/settings.ts | 34 +- src/lib/socket.ts | 152 +-- src/lib/store.ts | 110 +- src/lib/ticket.ts | 44 +- src/lib/tickets.ts | 100 +- src/lib/types.ts | 24 +- src/lib/upload.ts | 30 +- src/lib/utils.ts | 821 +++++++------- src/routes/(app)/+layout.server.ts | 104 +- src/routes/(app)/+layout.svelte | 11 +- .../(app)/[username]/(profile)/+layout.server.ts | 8 +- .../(app)/[username]/(profile)/+layout.svelte | 2 +- .../(app)/[username]/(profile)/+page.server.ts | 14 +- src/routes/(app)/[username]/(profile)/+page.ts | 6 +- .../[username]/(profile)/notes/+page.server.ts | 16 +- src/routes/(app)/[username]/+layout.server.ts | 30 +- .../[username]/accepted/[text]/+page.server.ts | 8 +- .../(app)/[username]/accepted/[text]/+page.svelte | 3 +- .../[username]/accepted/[text]/raw/+server.ts | 20 +- .../(app)/[username]/accepted/create/+page.svelte | 8 +- .../(app)/[username]/receive/+page.server.js | 20 +- src/routes/(app)/account/+server.ts | 14 +- src/routes/(app)/account/[id]/+page.server.ts | 28 +- src/routes/(app)/account/[id]/+page.svelte | 1 - src/routes/(app)/account/ark/+page.svelte | 42 +- src/routes/(app)/account/bitcoin/+page.svelte | 6 +- src/routes/(app)/account/cash/+page.svelte | 3 +- src/routes/(app)/account/new/+page.svelte | 8 +- src/routes/(app)/account/pass/+page.svelte | 7 +- src/routes/(app)/account/seed/+page.svelte | 3 +- src/routes/(app)/account/setup/+page.svelte | 6 +- src/routes/(app)/admin/+layout.server.ts | 22 +- src/routes/(app)/admin/+page.server.ts | 12 +- src/routes/(app)/apps/[pubkey]/+page.server.ts | 32 +- .../(app)/apps/[pubkey]/payments/+page.server.ts | 4 +- src/routes/(app)/apps/new/+page.server.ts | 54 +- src/routes/(app)/contacts/+server.ts | 14 +- src/routes/(app)/disable2fa/+server.ts | 14 +- src/routes/(app)/dm/+page.svelte | 1158 +++++++++++--------- src/routes/(app)/e/[id]/+page.server.ts | 16 +- .../ecash/[id]/[...version]/+layout.server.ts | 34 +- .../(app)/ecash/[id]/[...version]/+page.server.ts | 36 +- .../ecash/[id]/[...version]/swap/+page.server.ts | 2 +- src/routes/(app)/ecash/mint/+server.ts | 4 +- src/routes/(app)/ecash/post/+server.ts | 14 +- src/routes/(app)/email/+server.ts | 4 +- src/routes/(app)/enable2fa/+server.ts | 6 +- src/routes/(app)/events/+server.ts | 14 +- src/routes/(app)/fund/[id]/+layout.server.ts | 14 +- src/routes/(app)/fund/[id]/+page.server.ts | 16 +- src/routes/(app)/fund/[id]/+page.svelte | 4 +- src/routes/(app)/fund/[id]/access/+page.server.ts | 24 +- .../[id]/authorize/[...amount]/+page.server.ts | 46 +- src/routes/(app)/fund/[id]/gift/+page.server.ts | 8 +- src/routes/(app)/fund/[id]/gift/+page.svelte | 3 +- .../fund/[id]/sweep/[...amount]/+page.server.ts | 95 +- .../(app)/fund/[id]/withdraw/+page.server.ts | 34 +- src/routes/(app)/fund/[id]/withdraw/+server.ts | 2 +- src/routes/(app)/funder/+page.svelte | 18 +- src/routes/(app)/invoice/+page.server.ts | 38 +- src/routes/(app)/invoice/+server.ts | 14 +- src/routes/(app)/invoice/[id]/+layout.server.ts | 66 +- src/routes/(app)/invoice/[id]/+page.server.ts | 2 +- src/routes/(app)/invoice/[id]/memo/+page.server.ts | 2 +- src/routes/(app)/invoice/[id]/tip/+page.server.ts | 84 +- src/routes/(app)/invoice/[id]/tip/+page.svelte | 6 +- src/routes/(app)/invoice/sent/+page.svelte | 3 +- src/routes/(app)/invoices/+page.server.ts | 4 +- src/routes/(app)/items/[id]/+page.server.ts | 24 +- src/routes/(app)/items/[id]/+page.svelte | 2 +- src/routes/(app)/items/create/+page.server.ts | 20 +- src/routes/(app)/items/create/+page.svelte | 2 +- src/routes/(app)/ln/[lnurl]/+page.server.ts | 226 ++-- src/routes/(app)/locations/+server.ts | 16 +- src/routes/(app)/logout/+page.server.ts | 24 +- src/routes/(app)/messages/+page.server.ts | 66 +- src/routes/(app)/messages/+page.svelte | 2 +- .../(app)/messages/[recipient]/+page.server.ts | 32 +- src/routes/(app)/messages/[recipient]/+page.svelte | 12 +- src/routes/(app)/otpsecret/+server.ts | 18 +- src/routes/(app)/password/+server.ts | 4 +- .../pay/[username]/[...amount]/+page.server.ts | 106 +- src/routes/(app)/payment/[id]/+page.server.ts | 6 +- src/routes/(app)/payment/[id]/bump/+server.ts | 14 +- .../(app)/payment/[id]/plain/+page.server.ts | 4 +- src/routes/(app)/payment/[id]/plain/+page.svelte | 3 +- src/routes/(app)/payment/[id]/print/+server.ts | 4 +- .../(app)/payments/[...page]/+page.server.ts | 46 +- src/routes/(app)/payments/[...page]/+server.ts | 20 +- src/routes/(app)/pin/+server.ts | 4 +- src/routes/(app)/pos/+page.server.ts | 36 +- src/routes/(app)/pos/+page.svelte | 73 +- src/routes/(app)/post/[...path]/+server.ts | 12 +- src/routes/(app)/printer/+page.server.ts | 24 +- src/routes/(app)/qr/[text]/+page.server.ts | 4 +- src/routes/(app)/qr/[text]/+page.svelte | 3 +- src/routes/(app)/qr/[text]/[icon]/+server.ts | 48 +- src/routes/(app)/qr/[text]/raw/+server.ts | 21 +- src/routes/(app)/qr/create/+page.svelte | 8 +- .../(app)/receive/[...token]/+page.server.ts | 16 +- src/routes/(app)/receive/[...token]/+page.svelte | 15 +- src/routes/(app)/reset/+server.ts | 4 +- src/routes/(app)/reset/[code]/+page.server.ts | 44 +- src/routes/(app)/reset/[code]/+page.svelte | 6 +- src/routes/(app)/scan/+layout@.svelte | 2 +- src/routes/(app)/scan/+page.svelte | 3 +- src/routes/(app)/send/+layout.server.ts | 4 +- src/routes/(app)/send/[...text]/+page.server.ts | 22 +- src/routes/(app)/send/[...text]/+page.svelte | 3 +- .../(app)/send/ark/[address]/+layout.server.ts | 36 +- .../(app)/send/ark/[address]/+page.server.ts | 10 +- src/routes/(app)/send/ark/[address]/+page.svelte | 6 +- .../[amount]/[...feeRate]/+page.server.ts | 81 +- .../(app)/send/bitcoin/[address]/+layout.server.ts | 34 +- .../(app)/send/bitcoin/[address]/+page.server.ts | 10 +- .../(app)/send/bitcoin/[address]/+page.svelte | 9 +- .../[amount]/[...feeRate]/+page.server.ts | 112 +- .../[address]/[amount]/[...feeRate]/+page.svelte | 11 +- .../fund/[...name]/[...amount]/+page.server.ts | 30 +- src/routes/(app)/send/invoice/[id]/+page.server.ts | 118 +- .../lightning/[payreq]/[...memo]/+page.server.ts | 108 +- .../send/lightning/[payreq]/[...memo]/+page.svelte | 25 +- .../send/zap/[id]/[...amount]/+page.server.ts | 64 +- .../(app)/send/zap/[id]/[...amount]/+page.svelte | 4 +- src/routes/(app)/sent/[id]/+page.server.ts | 6 +- src/routes/(app)/sent/[id]/+page.svelte | 9 +- src/routes/(app)/settings/+layout.server.ts | 14 +- src/routes/(app)/settings/+layout.svelte | 40 +- src/routes/(app)/settings/account/+page.server.ts | 8 +- src/routes/(app)/settings/account/+page.svelte | 3 +- src/routes/(app)/settings/nostr/+page.server.ts | 8 +- src/routes/(app)/settings/nostr/+page.svelte | 63 +- src/routes/(app)/settings/profile/+page.server.ts | 2 +- src/routes/(app)/settings/profile/+page.svelte | 11 +- src/routes/(app)/settings/security/+page.server.ts | 2 +- src/routes/(app)/settings/security/+page.svelte | 19 +- src/routes/(app)/settings/shopify/+page.server.ts | 2 +- src/routes/(app)/settings/square/+page.server.ts | 20 +- src/routes/(app)/sign/+page.server.ts | 24 +- src/routes/(app)/subscription/+server.ts | 6 +- src/routes/(app)/subscription/delete/+server.ts | 6 +- src/routes/(app)/thread/[id]/+page.server.ts | 14 +- src/routes/(app)/verify/[code]/+page.server.ts | 15 +- src/routes/(app)/withdraw/+server.ts | 10 +- src/routes/(static)/+layout.server.ts | 24 +- src/routes/(static)/bounties/+page.server.ts | 78 +- src/routes/(static)/docs/+page.server.ts | 8 +- src/routes/(static)/docs/+page.svelte | 10 +- src/routes/(static)/forgot/+page.server.ts | 18 +- src/routes/(static)/forgot/+page.svelte | 3 +- src/routes/(static)/login/+page.server.ts | 115 +- src/routes/(static)/login/+page.svelte | 14 +- src/routes/(static)/login/[token]/+page.server.ts | 14 +- src/routes/(static)/register/+page.server.ts | 30 +- src/routes/(static)/register/+page.svelte | 16 +- src/routes/(static)/support/+page.svelte | 12 +- src/routes/+layout.server.ts | 102 +- src/routes/+layout.svelte | 19 +- src/routes/+layout.ts | 72 +- src/routes/+page.server.ts | 2 +- src/routes/about/+server.ts | 2 +- src/routes/amber/[id]/[pubkey]/+server.ts | 4 +- src/routes/map/+page.server.ts | 10 +- src/routes/map/+page.svelte | 3 +- src/routes/phtest/+page.svelte | 4 +- src/routes/start/+server.ts | 14 +- src/routes/test/ark/send/+server.ts | 124 +-- src/service-worker.js | 58 +- 220 files changed, 4833 insertions(+), 4296 deletions(-) (limited to 'src') diff --git a/src/app.css b/src/app.css index f310b65c..62997d50 100644 --- a/src/app.css +++ b/src/app.css @@ -6,21 +6,23 @@ /* 2) Register DaisyUI themes */ @plugin "daisyui" { - themes: lofi --default, black --prefersdark; + themes: + lofi --default, + black --prefersdark; } @plugin "daisyui/theme" { name: "lofi"; default: true; - --gradient: #C3CFE2; + --gradient: #c3cfe2; --color-error: #b91c1c; } @plugin "daisyui/theme" { name: "black"; prefersdark: true; - --gradient: #C3CFE2; - --color-secondary: #CCC; + --gradient: #c3cfe2; + --color-secondary: #ccc; --color-error: #f87171; } @@ -36,7 +38,8 @@ @font-face { font-family: "PublicSans"; - src: url("/fonts/Public_Sans/static/PublicSans-Regular.ttf") format("truetype"); + src: url("/fonts/Public_Sans/static/PublicSans-Regular.ttf") + format("truetype"); } :root { @@ -47,13 +50,22 @@ --toastBackground: #292929; } -html { font-family: PublicSans, sans-serif; } +html { + font-family: PublicSans, sans-serif; +} -::selection { color: black; background: #eaf2ff; } -:fullscreen { background-color: white; } +::selection { + color: black; + background: #eaf2ff; +} +:fullscreen { + background-color: white; +} /* Note: @apply-ing DaisyUI component classes requires components reference */ -.input { @apply input-lg input-primary h-16 px-4; } +.input { + @apply input-lg input-primary h-16 px-4; +} /* If you want to *turn* native inputs into DaisyUI inputs via @apply: */ input[type="text"], @@ -65,14 +77,22 @@ input[type="url"], input[type="tel"] { @apply input input-lg input-primary h-16 px-4; } -textarea { @apply textarea textarea-lg textarea-primary py-4; } -select { @apply select select-lg select-primary h-16 px-4; } +textarea { + @apply textarea textarea-lg textarea-primary py-4; +} +select { + @apply select select-lg select-primary h-16 px-4; +} -input:not([type="range"]), textarea, select, .input { +input:not([type="range"]), +textarea, +select, +.input { @apply rounded-2xl w-full; } -input.clean, textarea.clean { +input.clean, +textarea.clean { border: none !important; outline: none !important; padding: 0 !important; @@ -81,8 +101,12 @@ input.clean, textarea.clean { box-shadow: none !important; } -button { cursor: pointer !important; } -.btn { @apply rounded-2xl btn-lg btn-block outline-none h-16 cursor-pointer; } +button { + cursor: pointer !important; +} +.btn { + @apply rounded-2xl btn-lg btn-block outline-none h-16 cursor-pointer; +} @layer components { :where(input, textarea, select, .input, .textarea, .select) { diff --git a/src/components/About.svelte b/src/components/About.svelte index 5cc3603c..0c8be717 100644 --- a/src/components/About.svelte +++ b/src/components/About.svelte @@ -1,5 +1,6 @@ {#if typeof amount !== "undefined"} @@ -35,10 +34,7 @@ aria-label="Toggle currency display" > {#if !canShowFiat} - {/if} {#if canShowFiat} diff --git a/src/components/AppForm.svelte b/src/components/AppForm.svelte index 026e4aba..2f65372c 100644 --- a/src/components/AppForm.svelte +++ b/src/components/AppForm.svelte @@ -146,11 +146,7 @@ >{$t("accounts.secret")}
-
diff --git a/src/components/Avatar.svelte b/src/components/Avatar.svelte index ca35fb35..c9bacf65 100644 --- a/src/components/Avatar.svelte +++ b/src/components/Avatar.svelte @@ -9,7 +9,12 @@ class?: string; } - let { user, size = 32, disabled = false, class: className = "" }: Props = $props(); + let { + user, + size = 32, + disabled = false, + class: className = "", + }: Props = $props(); let s = $derived(size.toString()); let link = $derived( @@ -23,7 +28,11 @@ let fallback = $derived( `${base}/punks/` + punk(user?.pubkey || user?.id || "aa"), ); - let tmp = $derived(($avatar as any)?.id && ($avatar as any).id === user?.id && ($avatar as any).src); + let tmp = $derived( + ($avatar as any)?.id && + ($avatar as any).id === user?.id && + ($avatar as any).src, + ); let src = $derived(tmp || profile || fallback); diff --git a/src/components/Balance.svelte b/src/components/Balance.svelte index b05d9ac5..41b55c7a 100644 --- a/src/components/Balance.svelte +++ b/src/components/Balance.svelte @@ -17,7 +17,12 @@ {#if show && user.haspin && $pin?.length !== 6} - { show = false; return ""; }} /> + { + show = false; + return ""; + }} + /> {/if}
diff --git a/src/components/DarkToggle.svelte b/src/components/DarkToggle.svelte index fdc79110..ebeaf762 100644 --- a/src/components/DarkToggle.svelte +++ b/src/components/DarkToggle.svelte @@ -11,10 +11,11 @@ class="flex justify-center items-center hover:opacity-80 gap-2 font-bold" onclick={dark} > - + > {$t(`nav.${$theme === "dark" ? "light" : "dark"}`)} diff --git a/src/components/FaqCard.svelte b/src/components/FaqCard.svelte index a2d13bc8..c1739495 100644 --- a/src/components/FaqCard.svelte +++ b/src/components/FaqCard.svelte @@ -3,7 +3,7 @@ // this ID will be used to get the strings from the locale // question is at faq.[questionID].question - + interface Props { // answer is at faq.[questionID].answer questionID: any; diff --git a/src/components/Footer.svelte b/src/components/Footer.svelte index 27a57e55..92930300 100644 --- a/src/components/Footer.svelte +++ b/src/components/Footer.svelte @@ -73,5 +73,10 @@
- + diff --git a/src/components/HowItWorksCard.svelte b/src/components/HowItWorksCard.svelte index d69d7666..1012af46 100644 --- a/src/components/HowItWorksCard.svelte +++ b/src/components/HowItWorksCard.svelte @@ -1,9 +1,11 @@ -
- +

{$t("howItWorks." + stepID + ".header")}

{$t("howItWorks." + stepID + ".description")} diff --git a/src/components/ItemForm.svelte b/src/components/ItemForm.svelte index d087e2bf..7215a239 100644 --- a/src/components/ItemForm.svelte +++ b/src/components/ItemForm.svelte @@ -1,5 +1,5 @@ @@ -12,10 +13,11 @@ {$t("landing.header_before_icon")} {$t("landing.header_with_icon")} - + > {$t("landing.header_after_icon")} diff --git a/src/components/LandingInfoCard.svelte b/src/components/LandingInfoCard.svelte index ca16a5a4..cb16c5dd 100644 --- a/src/components/LandingInfoCard.svelte +++ b/src/components/LandingInfoCard.svelte @@ -6,7 +6,7 @@ title, description = undefined, order = undefined, - children + children, } = $props(); diff --git a/src/components/Lnurlw.svelte b/src/components/Lnurlw.svelte index ac79b101..006fda8b 100644 --- a/src/components/Lnurlw.svelte +++ b/src/components/Lnurlw.svelte @@ -89,7 +89,10 @@ disabled={submitting} onkeydown={setMax} >{#if $fiat} - Max {f((Math.floor(maxWithdrawable / 1000) * rate) / sats, currency)} + Max {f( + (Math.floor(maxWithdrawable / 1000) * rate) / sats, + currency, + )} {:else} Max ⚡️{s(Math.floor(maxWithdrawable / 1000))} {/if} -

diff --git a/src/components/LocationsMap.svelte b/src/components/LocationsMap.svelte index 26f32ee8..3ec50e38 100644 --- a/src/components/LocationsMap.svelte +++ b/src/components/LocationsMap.svelte @@ -181,21 +181,22 @@ maplibrePromise = new Promise((resolve, reject) => { const existing = document.querySelector("script[data-maplibre]"); if (existing) { - if ((window as any).maplibregl) return resolve((window as any).maplibregl); - existing.addEventListener("load", () => resolve((window as any).maplibregl)); + if ((window as any).maplibregl) + return resolve((window as any).maplibregl); + existing.addEventListener("load", () => + resolve((window as any).maplibregl), + ); existing.addEventListener("error", reject); return; } const css = document.createElement("link"); css.rel = "stylesheet"; - css.href = - "https://unpkg.com/maplibre-gl@5.7.1/dist/maplibre-gl.css"; + css.href = "https://unpkg.com/maplibre-gl@5.7.1/dist/maplibre-gl.css"; document.head.appendChild(css); const script = document.createElement("script"); - script.src = - "https://unpkg.com/maplibre-gl@5.7.1/dist/maplibre-gl.js"; + script.src = "https://unpkg.com/maplibre-gl@5.7.1/dist/maplibre-gl.js"; script.async = true; script.defer = true; script.dataset.maplibre = "true"; @@ -299,11 +300,12 @@ >
@@ -316,10 +318,12 @@
{#if search} {:else} - + {/if}
@@ -350,9 +354,11 @@ aria-label={timeout ? "Resume map" : "Pause map"} > {#if timeout} - + {:else} - + {/if}
diff --git a/src/components/Menu.svelte b/src/components/Menu.svelte index 7f04dff0..9b49a263 100644 --- a/src/components/Menu.svelte +++ b/src/components/Menu.svelte @@ -66,7 +66,8 @@
diff --git a/src/components/Nostr.svelte b/src/components/Nostr.svelte index e7fd0e58..74e85faf 100644 --- a/src/components/Nostr.svelte +++ b/src/components/Nostr.svelte @@ -32,9 +32,8 @@ let connectUrl: string | undefined = $state(); let nostrConnect = async () => { const { Relay } = await import("nostr-tools/relay"); - const { nip04, generateSecretKey, getPublicKey } = await import( - "nostr-tools" - ); + const { nip04, generateSecretKey, getPublicKey } = + await import("nostr-tools"); const { getConversationKey, decrypt } = await import("nostr-tools/nip44"); let connectionSecret = crypto.randomUUID(); @@ -119,9 +118,7 @@ {#if $eventToSign && !$signer?.ready} -
+
diff --git a/src/components/Numpad.svelte b/src/components/Numpad.svelte index 6a9f3c6e..57e8dd24 100644 --- a/src/components/Numpad.svelte +++ b/src/components/Numpad.svelte @@ -77,14 +77,16 @@ if (fiat) { const numericFiat = parseFloat((fiatDigits || "0").replace(/\D/g, "")) / 100; - amountFiat = isFinite(numericFiat) ? numericFiat.toFixed(2) : "0.00" as any; + amountFiat = isFinite(numericFiat) + ? numericFiat.toFixed(2) + : ("0.00" as any); amount = numericFiat ? Math.round((numericFiat * sats) / rate) : 0; } else { // integer-only sats, already normalized const a = parseInt((html || "0").replace(/[^\d]/g, "")) || 0; amount = a || 0; const fval = amount ? (amount * rate) / sats : 0; - amountFiat = fval ? fval.toFixed(2) : "0.00" as any; + amountFiat = fval ? fval.toFixed(2) : ("0.00" as any); } }; @@ -211,7 +213,9 @@ element && (element.innerHTML = html); } else { // going -> fiat - const cents = Math.round((parseFloat(String(amountFiat) || "0") || 0) * 100); + const cents = Math.round( + (parseFloat(String(amountFiat) || "0") || 0) * 100, + ); fiatDigits = Math.max(0, cents).toString(); fiat = true; html = formatFiatFromDigits(fiatDigits, decimalChar); @@ -255,7 +259,9 @@ onMount(() => { fiat = $fiatStore; if (fiat) { - const cents = Math.round((parseFloat(String(amountFiat) || "0") || 0) * 100); + const cents = Math.round( + (parseFloat(String(amountFiat) || "0") || 0) * 100, + ); fiatDigits = Math.max(0, cents).toString(); html = formatFiatFromDigits(fiatDigits, decimalChar); // keep html in sync on mount } else { diff --git a/src/components/Password.svelte b/src/components/Password.svelte index 02d65948..2c2319f6 100644 --- a/src/components/Password.svelte +++ b/src/components/Password.svelte @@ -23,9 +23,7 @@ }; -
+
diff --git a/src/components/Payments.svelte b/src/components/Payments.svelte index ff3f0d19..046356ca 100644 --- a/src/components/Payments.svelte +++ b/src/components/Payments.svelte @@ -27,7 +27,9 @@
{#if $fiat} -
{f(Math.abs(amount) * (p.rate / sats), p.currency, locale)}
+
+ {f(Math.abs(amount) * (p.rate / sats), p.currency, locale)} +
{:else}
{:else if p.type === types.ark} - Ark + Ark {/if}
diff --git a/src/components/Pinpad.svelte b/src/components/Pinpad.svelte index 524e75f8..15dbc53e 100644 --- a/src/components/Pinpad.svelte +++ b/src/components/Pinpad.svelte @@ -1,5 +1,6 @@ {#if settingAmount} -
+
diff --git a/src/components/SetMemo.svelte b/src/components/SetMemo.svelte index 2c3fd617..54921f56 100644 --- a/src/components/SetMemo.svelte +++ b/src/components/SetMemo.svelte @@ -12,9 +12,7 @@ {#if settingMemo} -
+
diff --git a/src/components/SetType.svelte b/src/components/SetType.svelte index 4734cb07..d06ca345 100644 --- a/src/components/SetType.svelte +++ b/src/components/SetType.svelte @@ -14,13 +14,10 @@ activeOnly = false, t, }: any = $props(); - {#if settingType} -
+
diff --git a/src/components/Slider.svelte b/src/components/Slider.svelte index 5b1573f7..846d32a6 100644 --- a/src/components/Slider.svelte +++ b/src/components/Slider.svelte @@ -18,4 +18,3 @@ class="range w-full" oninput={handle} /> - diff --git a/src/components/Success.svelte b/src/components/Success.svelte index 6a0e8eae..ae54b6c1 100644 --- a/src/components/Success.svelte +++ b/src/components/Success.svelte @@ -2,7 +2,14 @@ import { browser } from "$app/environment"; import { scale } from "svelte/transition"; import Amount from "$comp/Amount.svelte"; - const { amount, tip = undefined, rate, currency, locale, title = "" }: any = $props(); + const { + amount, + tip = undefined, + rate, + currency, + locale, + title = "", + }: any = $props(); let ConfettiComp: any = $state(null); let loaded = $state(false); @@ -57,7 +64,8 @@
-
+ >
diff --git a/src/components/WalletPass.svelte b/src/components/WalletPass.svelte index f6d611d0..9e0b6cd2 100644 --- a/src/components/WalletPass.svelte +++ b/src/components/WalletPass.svelte @@ -1,5 +1,5 @@ -
+
@@ -75,7 +73,8 @@ id="rememberFor" class="w-full" value={rememberForMs} - onchange={(e) => (rememberForMs = Number((e.target as HTMLSelectElement).value))} + onchange={(e) => + (rememberForMs = Number((e.target as HTMLSelectElement).value))} > {#each rememberForOptions as option} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index bd9bbc01..780e62ad 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -5,10 +5,10 @@ import type { Handle } from "@sveltejs/kit"; setIpGetter(() => ipStore.getStore()); export const handle: Handle = async ({ event, resolve }) => { - const ip = - event.request.headers.get("cf-connecting-ip") || - event.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || - event.getClientAddress(); + const ip = + event.request.headers.get("cf-connecting-ip") || + event.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || + event.getClientAddress(); - return ipStore.run(ip, () => resolve(event)); + return ipStore.run(ip, () => resolve(event)); }; diff --git a/src/lib/ark.ts b/src/lib/ark.ts index d045049e..51e378c3 100644 --- a/src/lib/ark.ts +++ b/src/lib/ark.ts @@ -4,8 +4,8 @@ import { post } from "$lib/utils"; import { get } from "svelte/store"; export const arkServerUrl = { - bitcoin: "https://arkade.computer", - regtest: "http://localhost:7070", + bitcoin: "https://arkade.computer", + regtest: "http://localhost:7070", }[PUBLIC_COINOS_NETWORK || "regtest"]; export const arkkey = persistLocal("arkkey", ""); @@ -16,166 +16,166 @@ let walletInstance: any; let walletKey: string | undefined; export const getWallet = async () => { - const key = get(arkkey); - if (!key) { - walletInstance = undefined; - walletKey = undefined; - return; - } - if (walletInstance && walletKey === key) return walletInstance; - - const { SingleKey, Wallet } = await loadArkSdk(); - const identity = SingleKey.fromHex(key); - - walletInstance = await Wallet.create({ - identity, - arkServerUrl, - }); - walletKey = key; - return walletInstance; + const key = get(arkkey); + if (!key) { + walletInstance = undefined; + walletKey = undefined; + return; + } + if (walletInstance && walletKey === key) return walletInstance; + + const { SingleKey, Wallet } = await loadArkSdk(); + const identity = SingleKey.fromHex(key); + + walletInstance = await Wallet.create({ + identity, + arkServerUrl, + }); + walletKey = key; + return walletInstance; }; export const getAddress = async (): Promise => { - const wallet = await getWallet(); - return wallet.getAddress(); + const wallet = await getWallet(); + return wallet.getAddress(); }; export const getBalance = async (): Promise<{ available: number }> => { - const wallet = await getWallet(); - return wallet.getBalance(); + const wallet = await getWallet(); + return wallet.getBalance(); }; export const getVtxos = async () => { - const wallet = await getWallet(); - if (!wallet) return []; - return wallet.getVtxos({ spendableOnly: false }); + const wallet = await getWallet(); + if (!wallet) return []; + return wallet.getVtxos({ spendableOnly: false }); }; export const syncTransactions = async (aid: string) => { - const wallet = await getWallet(); - if (!wallet) return; - - const transactions: any[] = []; - - const history = await wallet.getTransactionHistory(); - const historyTxids = new Set(); - console.log("arkSync: history", history.length, history); - - for (const tx of history) { - if (tx.key.arkTxid) historyTxids.add(tx.key.arkTxid); - if (tx.key.commitmentTxid) historyTxids.add(tx.key.commitmentTxid); - transactions.push({ - arkTxid: tx.key.arkTxid || null, - commitmentTxid: tx.key.commitmentTxid || null, - amount: tx.type === "RECEIVED" ? tx.amount : -tx.amount, - settled: tx.settled, - createdAt: tx.createdAt, - }); - } - - // Include preconfirmed VTXOs not yet in transaction history - const vtxos = await wallet.getVtxos({ spendableOnly: false }); - console.log("arkSync: vtxos", vtxos.length, vtxos); - for (const v of vtxos) { - if (v.virtualStatus?.state !== "preconfirmed") continue; - const id = v.arkTxId || v.txid; - if (!id || historyTxids.has(id)) continue; - historyTxids.add(id); - transactions.push({ - arkTxid: v.arkTxId || null, - commitmentTxid: v.txid || null, - amount: Number(v.value), - settled: false, - createdAt: v.createdAt ? new Date(v.createdAt).getTime() : Date.now(), - }); - } - - if (!transactions.length) return; - - return post("/post/ark/sync", { transactions, aid }); + const wallet = await getWallet(); + if (!wallet) return; + + const transactions: any[] = []; + + const history = await wallet.getTransactionHistory(); + const historyTxids = new Set(); + console.log("arkSync: history", history.length, history); + + for (const tx of history) { + if (tx.key.arkTxid) historyTxids.add(tx.key.arkTxid); + if (tx.key.commitmentTxid) historyTxids.add(tx.key.commitmentTxid); + transactions.push({ + arkTxid: tx.key.arkTxid || null, + commitmentTxid: tx.key.commitmentTxid || null, + amount: tx.type === "RECEIVED" ? tx.amount : -tx.amount, + settled: tx.settled, + createdAt: tx.createdAt, + }); + } + + // Include preconfirmed VTXOs not yet in transaction history + const vtxos = await wallet.getVtxos({ spendableOnly: false }); + console.log("arkSync: vtxos", vtxos.length, vtxos); + for (const v of vtxos) { + if (v.virtualStatus?.state !== "preconfirmed") continue; + const id = v.arkTxId || v.txid; + if (!id || historyTxids.has(id)) continue; + historyTxids.add(id); + transactions.push({ + arkTxid: v.arkTxId || null, + commitmentTxid: v.txid || null, + amount: Number(v.value), + settled: false, + createdAt: v.createdAt ? new Date(v.createdAt).getTime() : Date.now(), + }); + } + + if (!transactions.length) return; + + return post("/post/ark/sync", { transactions, aid }); }; export const vtxoKey = (v: { txid: string; vout: number }) => - `${v.txid}:${v.vout}`; + `${v.txid}:${v.vout}`; export const getProcessedVtxos = (): Set => { - try { - return new Set( - JSON.parse(localStorage.getItem("processedArkVtxos") || "[]"), - ); - } catch { - return new Set(); - } + try { + return new Set( + JSON.parse(localStorage.getItem("processedArkVtxos") || "[]"), + ); + } catch { + return new Set(); + } }; export const addProcessedVtxos = (keys: string[]) => { - const processed = getProcessedVtxos(); - for (const k of keys) processed.add(k); - const arr = [...processed].slice(-200); - localStorage.setItem("processedArkVtxos", JSON.stringify(arr)); + const processed = getProcessedVtxos(); + for (const k of keys) processed.add(k); + const arr = [...processed].slice(-200); + localStorage.setItem("processedArkVtxos", JSON.stringify(arr)); }; // Flag to suppress notifyIncomingFunds during sends export let arkSending = false; export const sendArk = async (address: string, amount: number) => { - const wallet = await getWallet(); - if (!wallet) throw new Error("Ark wallet not available"); + const wallet = await getWallet(); + if (!wallet) throw new Error("Ark wallet not available"); - // Suppress incoming notifications during send to prevent - // change VTXOs from being credited as incoming payments - arkSending = true; + // Suppress incoming notifications during send to prevent + // change VTXOs from being credited as incoming payments + arkSending = true; - try { - const txid = await wallet.sendBitcoin({ address, amount }); + try { + const txid = await wallet.sendBitcoin({ address, amount }); - // Mark post-send VTXOs as processed so notifyIncomingFunds - // ignores change VTXOs from this send - const vtxos = await wallet.getVtxos({ spendableOnly: false }); - if (vtxos?.length) addProcessedVtxos(vtxos.map(vtxoKey)); + // Mark post-send VTXOs as processed so notifyIncomingFunds + // ignores change VTXOs from this send + const vtxos = await wallet.getVtxos({ spendableOnly: false }); + if (vtxos?.length) addProcessedVtxos(vtxos.map(vtxoKey)); - return txid; - } finally { - arkSending = false; - } + return txid; + } finally { + arkSending = false; + } }; export const sendArkViaForward = async ({ - serverArkAddress, - amount, - aid, - forward, - user, + serverArkAddress, + amount, + aid, + forward, + user, }: { - serverArkAddress: string; - amount: number; - aid: string; - forward: string; - user: any; + serverArkAddress: string; + amount: number; + aid: string; + forward: string; + user: any; }) => { - const txid = await sendArk(serverArkAddress, amount); + const txid = await sendArk(serverArkAddress, amount); - await post("/post/ark/vault-send", { hash: txid, amount, aid }); + await post("/post/ark/vault-send", { hash: txid, amount, aid }); - const inv = await post("/post/invoice", { - invoice: { type: "ark", amount, forward, aid }, - user, - }); + const inv = await post("/post/invoice", { + invoice: { type: "ark", amount, forward, aid }, + user, + }); - return post("/post/ark/receive", { amount, hash: txid, iid: inv.id }); + return post("/post/ark/receive", { amount, hash: txid, iid: inv.id }); }; export const settle = async () => { - const wallet = await getWallet(); - if (!wallet) return; + const wallet = await getWallet(); + if (!wallet) return; - const balance = await wallet.getBalance(); - if (!balance.preconfirmed) return; + const balance = await wallet.getBalance(); + if (!balance.preconfirmed) return; - await wallet.settle(); + await wallet.settle(); - // Mark post-settle VTXOs as processed so notifyIncomingFunds - // ignores renewed VTXOs (they get new txids after an Ark round) - const vtxos = await wallet.getVtxos({ spendableOnly: false }); - if (vtxos?.length) addProcessedVtxos(vtxos.map(vtxoKey)); + // Mark post-settle VTXOs as processed so notifyIncomingFunds + // ignores renewed VTXOs (they get new txids after an Ark round) + const vtxos = await wallet.getVtxos({ spendableOnly: false }); + if (vtxos?.length) addProcessedVtxos(vtxos.map(vtxoKey)); }; diff --git a/src/lib/bip21.ts b/src/lib/bip21.ts index 4689246e..842782a5 100644 --- a/src/lib/bip21.ts +++ b/src/lib/bip21.ts @@ -1,50 +1,50 @@ export function decode(uri, allowedSchemes = ["bitcoin", "liquidnetwork"]) { - // Determine the scheme from the URI - const split = uri.indexOf(":"); - if (split === -1) { - throw new Error("Invalid URI: Missing scheme"); - } - - const urnScheme = uri.slice(0, split).toLowerCase(); - if (!allowedSchemes.includes(urnScheme)) { - throw new Error(`Invalid scheme: ${urnScheme}`); - } - - // Extract address and query string - const queryStart = uri.indexOf("?"); - const address = uri.slice( - split + 1, - queryStart === -1 ? undefined : queryStart, - ); - const query = queryStart === -1 ? "" : uri.slice(queryStart + 1); - const options = Object.fromEntries(new URLSearchParams(query)); - - if (options.amount) { - const n = Number(options.amount); - if (!Number.isFinite(n)) throw new Error("Invalid amount"); - if (n < 0) throw new Error("Invalid amount"); - } - - return { scheme: urnScheme, address, options }; + // Determine the scheme from the URI + const split = uri.indexOf(":"); + if (split === -1) { + throw new Error("Invalid URI: Missing scheme"); + } + + const urnScheme = uri.slice(0, split).toLowerCase(); + if (!allowedSchemes.includes(urnScheme)) { + throw new Error(`Invalid scheme: ${urnScheme}`); + } + + // Extract address and query string + const queryStart = uri.indexOf("?"); + const address = uri.slice( + split + 1, + queryStart === -1 ? undefined : queryStart, + ); + const query = queryStart === -1 ? "" : uri.slice(queryStart + 1); + const options = Object.fromEntries(new URLSearchParams(query)); + + if (options.amount) { + const n = Number(options.amount); + if (!Number.isFinite(n)) throw new Error("Invalid amount"); + if (n < 0) throw new Error("Invalid amount"); + } + + return { scheme: urnScheme, address, options }; } export function encode( - address, - options = { amount: "" }, - urnScheme = "bitcoin", + address, + options = { amount: "" }, + urnScheme = "bitcoin", ) { - if (!address || typeof address !== "string") { - throw new TypeError("Invalid address"); - } + if (!address || typeof address !== "string") { + throw new TypeError("Invalid address"); + } - if (!["bitcoin", "liquidnetwork"].includes(urnScheme)) { - throw new Error(`Unsupported scheme: ${urnScheme}`); - } + if (!["bitcoin", "liquidnetwork"].includes(urnScheme)) { + throw new Error(`Unsupported scheme: ${urnScheme}`); + } - const n = Number(options?.amount); - if (!Number.isFinite(n)) throw new Error("Invalid amount"); - if (n < 0) throw new Error("Invalid amount"); + const n = Number(options?.amount); + if (!Number.isFinite(n)) throw new Error("Invalid amount"); + if (n < 0) throw new Error("Invalid amount"); - const query = new URLSearchParams(options).toString(); - return `${urnScheme}:${address}${query ? `?${query}` : ""}`; + const query = new URLSearchParams(options).toString(); + return `${urnScheme}:${address}${query ? `?${query}` : ""}`; } diff --git a/src/lib/cache.ts b/src/lib/cache.ts index 079c561b..064b4632 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -1,10 +1,9 @@ - // src/lib/server/cache.ts let cachedData: any = null; async function fetchData() { // Fetch data from your API - const response = await fetch('https://api.example.com/data'); + const response = await fetch("https://api.example.com/data"); cachedData = await response.json(); } diff --git a/src/lib/handler.ts b/src/lib/handler.ts index baca442d..559761f0 100644 --- a/src/lib/handler.ts +++ b/src/lib/handler.ts @@ -2,12 +2,12 @@ import { goto } from "$app/navigation"; import { applyAction } from "$app/forms"; export default (toggle) => () => { - toggle(); - return async ({ result }) => { - if (result.type === "redirect") { - goto(result.location); - } else { - await applyAction(result); - } - }; + toggle(); + return async ({ result }) => { + if (result.type === "redirect") { + goto(result.location); + } else { + await applyAction(result); + } + }; }; diff --git a/src/lib/invoice.ts b/src/lib/invoice.ts index 91a69d88..015f2837 100644 --- a/src/lib/invoice.ts +++ b/src/lib/invoice.ts @@ -3,37 +3,37 @@ import { auth, post } from "$lib/utils"; import { redirect } from "@sveltejs/kit"; export default async ({ cookies, request }) => { - const form = await request.formData(); - const aid = form.get("aid") || cookies.get("aid"); + const form = await request.formData(); + const aid = form.get("aid") || cookies.get("aid"); - const amount = parseInt(form.get("amount")); - const rates = await getRates(); + const amount = parseInt(form.get("amount")); + const rates = await getRates(); - let invoice = { - aid, - amount, - items: JSON.parse(form.get("items")), - memo: form.get("memo"), - memoPrompt: form.get("memoPrompt"), - tip: form.get("tip") ?? null, - type: form.get("type"), - prompt: form.get("prompt") === "true", - rate: parseFloat(form.get("rate")) || rates[form.get("currency")], - id: undefined, - }; + let invoice = { + aid, + amount, + items: JSON.parse(form.get("items")), + memo: form.get("memo"), + memoPrompt: form.get("memoPrompt"), + tip: form.get("tip") ?? null, + type: form.get("type"), + prompt: form.get("prompt") === "true", + rate: parseFloat(form.get("rate")) || rates[form.get("currency")], + id: undefined, + }; - const user = { - username: form.get("username"), - currency: form.get("currency"), - }; + const user = { + username: form.get("username"), + currency: form.get("currency"), + }; - invoice = await post("/invoice", { invoice, user }, auth(cookies)); - const { id } = invoice; + invoice = await post("/invoice", { invoice, user }, auth(cookies)); + const { id } = invoice; - if (invoice.prompt && invoice.tip === null) - redirect(307, `/invoice/${id}/tip`); + if (invoice.prompt && invoice.tip === null) + redirect(307, `/invoice/${id}/tip`); - if (invoice.memoPrompt && !invoice.memo) { - redirect(307, `/invoice/${id}/memo`); - } else redirect(307, `/invoice/${id}`); + if (invoice.memoPrompt && !invoice.memo) { + redirect(307, `/invoice/${id}/memo`); + } else redirect(307, `/invoice/${id}`); }; diff --git a/src/lib/locales.ts b/src/lib/locales.ts index 282eac22..83e7a1bf 100644 --- a/src/lib/locales.ts +++ b/src/lib/locales.ts @@ -37,5 +37,5 @@ export default { it, nl, th, - pl + pl, }; diff --git a/src/lib/nip01.ts b/src/lib/nip01.ts index a5d3f4b5..b2718b6b 100644 --- a/src/lib/nip01.ts +++ b/src/lib/nip01.ts @@ -1,19 +1,21 @@ -import { PUBLIC_DM_RELAYS, PUBLIC_COINOS_RELAY } from '$env/static/public'; -const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); -const USERINFO_RELAYS = [ ...DM_RELAYS_LIST, PUBLIC_COINOS_RELAY ]; +import { PUBLIC_DM_RELAYS, PUBLIC_COINOS_RELAY } from "$env/static/public"; +const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(","); +const USERINFO_RELAYS = [...DM_RELAYS_LIST, PUBLIC_COINOS_RELAY]; -import { SimplePool } from 'nostr-tools/pool'; +import { SimplePool } from "nostr-tools/pool"; const pool = new SimplePool(); // Returns the content of the kind-0 event of the provided pubkey. export const getNostrUserInfo = async (pubkey: string) => { - const events = await pool.querySync( - USERINFO_RELAYS, { kinds: [0], limit: 1, authors: [pubkey] } - ); + const events = await pool.querySync(USERINFO_RELAYS, { + kinds: [0], + limit: 1, + authors: [pubkey], + }); - if (events.length == 0) { - return null; - } + if (events.length == 0) { + return null; + } - return JSON.parse(events[0].content); -} + return JSON.parse(events[0].content); +}; diff --git a/src/lib/nip07.ts b/src/lib/nip07.ts index 85f68377..4b32512f 100644 --- a/src/lib/nip07.ts +++ b/src/lib/nip07.ts @@ -1,41 +1,52 @@ -import { bytesToHex } from '@noble/hashes/utils.js'; -import { finalizeEvent } from 'nostr-tools/pure'; -import { getPrivateKey } from '$lib/nostr'; -import * as libnip44 from '$lib/nip44'; -import { u } from '$lib/nip44'; +import { bytesToHex } from "@noble/hashes/utils.js"; +import { finalizeEvent } from "nostr-tools/pure"; +import { getPrivateKey } from "$lib/nostr"; +import * as libnip44 from "$lib/nip44"; +import { u } from "$lib/nip44"; // Signs an event using NIP-07 if available, otherwise getPrivateKey() export const signEvent = async (event: any, user: any): Promise => { - if (await (window as any).nostr.getPublicKey() === user.pubkey) { - return (window as any).nostr.signEvent(event); - } else { - const sk = await getPrivateKey(user); - return finalizeEvent(event, sk); - } -} + if ((await (window as any).nostr.getPublicKey()) === user.pubkey) { + return (window as any).nostr.signEvent(event); + } else { + const sk = await getPrivateKey(user); + return finalizeEvent(event, sk); + } +}; // Encrypts an event in NIP-44 using NIP-07 if available, otherwise getPrivateKey() -export const encrypt = async (json: any, user: any, receiverPK: string): Promise => { - if (await (window as any).nostr.getPublicKey() === user.pubkey) { - return (window as any).nostr.nip44.encrypt( - receiverPK, JSON.stringify(json)); - } else { - const senderSK = await getPrivateKey(user); - const senderSKString = bytesToHex(senderSK); - const ckey = u.getConversationKey(senderSKString, receiverPK); - return libnip44.encrypt(JSON.stringify(json), ckey); - } -} +export const encrypt = async ( + json: any, + user: any, + receiverPK: string, +): Promise => { + if ((await (window as any).nostr.getPublicKey()) === user.pubkey) { + return (window as any).nostr.nip44.encrypt( + receiverPK, + JSON.stringify(json), + ); + } else { + const senderSK = await getPrivateKey(user); + const senderSKString = bytesToHex(senderSK); + const ckey = u.getConversationKey(senderSKString, receiverPK); + return libnip44.encrypt(JSON.stringify(json), ckey); + } +}; // Decrypts an event in NIP-44 using NIP-07 if available, otherwise getPrivateKey() -export const decrypt = async (encrypted: string, user: any, receiverPK: string): Promise => { - if (await (window as any).nostr.getPublicKey() === user.pubkey) { - return JSON.parse(await (window as any).nostr.nip44.decrypt( - receiverPK, encrypted)); - } else { - const senderSK = await getPrivateKey(user); - const senderSKString = bytesToHex(senderSK); - const ckey = u.getConversationKey(senderSKString, receiverPK); - return JSON.parse(libnip44.decrypt(encrypted, ckey)); - } -} +export const decrypt = async ( + encrypted: string, + user: any, + receiverPK: string, +): Promise => { + if ((await (window as any).nostr.getPublicKey()) === user.pubkey) { + return JSON.parse( + await (window as any).nostr.nip44.decrypt(receiverPK, encrypted), + ); + } else { + const senderSK = await getPrivateKey(user); + const senderSKString = bytesToHex(senderSK); + const ckey = u.getConversationKey(senderSKString, receiverPK); + return JSON.parse(libnip44.decrypt(encrypted, ckey)); + } +}; diff --git a/src/lib/nip11.ts b/src/lib/nip11.ts index af254b7e..6e827ccb 100644 --- a/src/lib/nip11.ts +++ b/src/lib/nip11.ts @@ -1,15 +1,15 @@ -import { fetchRelayInformation } from 'nostr-tools/nip11'; +import { fetchRelayInformation } from "nostr-tools/nip11"; // Accepts a list of relays, and returns a list of the relays that support all the provided NIPs. export const relaysSupporting = async (relays: string[], nips: number[]) => { - let supportingRelays = []; + let supportingRelays = []; - for (const relay of relays) { - const relayInfo = await fetchRelayInformation(relay); - if (nips.every(relay => relayInfo.supported_nips.includes(relay))) { - supportingRelays.push(relay); - } + for (const relay of relays) { + const relayInfo = await fetchRelayInformation(relay); + if (nips.every((relay) => relayInfo.supported_nips.includes(relay))) { + supportingRelays.push(relay); } + } - return supportingRelays; -} + return supportingRelays; +}; diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index b039ebf8..32a18bd8 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -1,84 +1,101 @@ -import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js'; - -import { finalizeEvent, getEventHash, generateSecretKey, getPublicKey } from 'nostr-tools/pure'; -import { SimplePool } from 'nostr-tools/pool'; -import { Relay } from 'nostr-tools/relay'; -import { unwrapEvent } from 'nostr-tools/nip17'; - -import { relaysSupporting } from '$lib/nip11'; -import { expired, expiration } from '$lib/nip40'; -import { encrypt, u } from '$lib/nip44'; -import { eventToSign, signer } from '$lib/store'; -import { get } from 'svelte/store'; +import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js"; + +import { + finalizeEvent, + getEventHash, + generateSecretKey, + getPublicKey, +} from "nostr-tools/pure"; +import { SimplePool } from "nostr-tools/pool"; +import { Relay } from "nostr-tools/relay"; +import { unwrapEvent } from "nostr-tools/nip17"; + +import { relaysSupporting } from "$lib/nip11"; +import { expired, expiration } from "$lib/nip40"; +import { encrypt, u } from "$lib/nip44"; +import { eventToSign, signer } from "$lib/store"; +import { get } from "svelte/store"; const TWO_DAYS = 2 * 24 * 60 * 60; -const randomTimestamp = () => Math.floor( - Date.now() / 1000 - Math.random() * TWO_DAYS); +const randomTimestamp = () => + Math.floor(Date.now() / 1000 - Math.random() * TWO_DAYS); const pool = new SimplePool(); let relayListCache = new Map(); -import { PUBLIC_DM_RELAYS } from '$env/static/public'; -const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); +import { PUBLIC_DM_RELAYS } from "$env/static/public"; +const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(","); const DM_FETCH_LIMIT = 256; // Ensures we have access to a secret key, triggering the signer modal if needed. // Returns the sk as Uint8Array, or null if using NIP-07 extension. const ensureSigner = async (userPubkey: string): Promise => { - // Check if NIP-07 extension is available and matches user - if ((window as any).nostr) { - try { - const extPubkey = await (window as any).nostr.getPublicKey(); - if (extPubkey === userPubkey) { - return null; // Use NIP-07 - } - } catch (e) { - // Extension didn't work, fall through to signer - } - } - - // Check if signer already has a valid sk (64 hex chars = 32 bytes) - const currentSigner: any = get(signer); - if (currentSigner?.ready && currentSigner?.params?.sk && currentSigner.params.sk.length === 64) { - return hexToBytes(currentSigner.params.sk); - } - - // Clear invalid signer state before triggering modal - if (currentSigner?.ready && (!currentSigner?.params?.sk || currentSigner.params.sk.length !== 64)) { - signer.set(undefined); + // Check if NIP-07 extension is available and matches user + if ((window as any).nostr) { + try { + const extPubkey = await (window as any).nostr.getPublicKey(); + if (extPubkey === userPubkey) { + return null; // Use NIP-07 + } + } catch { + // Extension didn't work, fall through to signer } - - // Trigger the signer modal with a dummy event - const dummyEvent = { kind: 14, content: 'DM encryption', tags: [], created_at: Math.floor(Date.now() / 1000) }; - eventToSign.set(dummyEvent); - - // Wait for signer to be ready - const signerValue = await new Promise((resolve, reject) => { - let unsubscribe: () => void; - unsubscribe = signer.subscribe((v: any) => { - if (v === 'cancel') { - setTimeout(() => unsubscribe?.()); - eventToSign.set(null); - reject(new Error('cancelled')); - } - if (v?.ready) { - setTimeout(() => unsubscribe?.()); - eventToSign.set(null); - resolve(v); - } - }); + } + + // Check if signer already has a valid sk (64 hex chars = 32 bytes) + const currentSigner: any = get(signer); + if ( + currentSigner?.ready && + currentSigner?.params?.sk && + currentSigner.params.sk.length === 64 + ) { + return hexToBytes(currentSigner.params.sk); + } + + // Clear invalid signer state before triggering modal + if ( + currentSigner?.ready && + (!currentSigner?.params?.sk || currentSigner.params.sk.length !== 64) + ) { + signer.set(undefined); + } + + // Trigger the signer modal with a dummy event + const dummyEvent = { + kind: 14, + content: "DM encryption", + tags: [], + created_at: Math.floor(Date.now() / 1000), + }; + eventToSign.set(dummyEvent); + + // Wait for signer to be ready + const signerValue = await new Promise((resolve, reject) => { + let unsubscribe: () => void; + unsubscribe = signer.subscribe((v: any) => { + if (v === "cancel") { + setTimeout(() => unsubscribe?.()); + eventToSign.set(null); + reject(new Error("cancelled")); + } + if (v?.ready) { + setTimeout(() => unsubscribe?.()); + eventToSign.set(null); + resolve(v); + } }); + }); - if (signerValue?.params?.sk) { - return hexToBytes(signerValue.params.sk); - } + if (signerValue?.params?.sk) { + return hexToBytes(signerValue.params.sk); + } - // If extension method was selected - if (signerValue?.method === 'extension') { - return null; // Use NIP-07 - } + // If extension method was selected + if (signerValue?.method === "extension") { + return null; // Use NIP-07 + } - throw new Error('No signing method available'); -} + throw new Error("No signing method available"); +}; // Create a NIP-17 message using a provided sender secret key. // If wrapPK is specified and different than receiverPK, @@ -87,11 +104,22 @@ const ensureSigner = async (userPubkey: string): Promise => { // If expiryDays is specified, the message will expire in that many days, // plus a random amount from 0 to 2 for security. // (expiration time = random creation time + expiryDays + 2 days) -export const createNIP17MessageSK = (text: string, senderSK: Uint8Array, receiverPK: string, wrapPK: string | null = null, expiryDays: number | null = null) => { - const rumour = createRumour(text, getPublicKey(senderSK), receiverPK); - const sealed = sealRumourSK(rumour, senderSK, wrapPK || receiverPK, expiryDays); - return giftWrap(sealed, wrapPK || receiverPK, expiryDays); -} +export const createNIP17MessageSK = ( + text: string, + senderSK: Uint8Array, + receiverPK: string, + wrapPK: string | null = null, + expiryDays: number | null = null, +) => { + const rumour = createRumour(text, getPublicKey(senderSK), receiverPK); + const sealed = sealRumourSK( + rumour, + senderSK, + wrapPK || receiverPK, + expiryDays, + ); + return giftWrap(sealed, wrapPK || receiverPK, expiryDays); +}; // Create a NIP-17 message using NIP-07, requiring no sender secret key. // If wrapPK is specified and different than receiverPK, @@ -100,241 +128,311 @@ export const createNIP17MessageSK = (text: string, senderSK: Uint8Array, receive // If expiryDays is specified, the message will expire in that many days, // plus a random amount from 0 to 2 for security. // (expiration time = random creation time + expiryDays + 2 days) -export const createNIP17MessageNIP07 = async (text: string, senderPK: string, receiverPK: string, wrapPK: string | null = null, expiryDays: number | null = null) => { - const rumour = createRumour(text, senderPK, receiverPK); - const sealed = await sealRumourNip07(rumour, wrapPK || receiverPK, expiryDays); - return giftWrap(sealed, wrapPK || receiverPK, expiryDays); -} +export const createNIP17MessageNIP07 = async ( + text: string, + senderPK: string, + receiverPK: string, + wrapPK: string | null = null, + expiryDays: number | null = null, +) => { + const rumour = createRumour(text, senderPK, receiverPK); + const sealed = await sealRumourNip07( + rumour, + wrapPK || receiverPK, + expiryDays, + ); + return giftWrap(sealed, wrapPK || receiverPK, expiryDays); +}; const createRumour = (text: string, senderPK: string, receiverPK: string) => { - const rumour: any = { - kind: 14, // rumour - created_at: Math.floor(Date.now() / 1000), - tags: [["p", receiverPK]], - pubkey: senderPK, - content: text - }; - rumour.id = getEventHash(rumour); // do NOT sign rumours! - return rumour; -} - -const sealRumourSK = (rumour: any, senderSK: Uint8Array, receiverPK: string, expiryDays: number | null = null) => { - const senderSKString = bytesToHex(senderSK); - const conversationKey = u.getConversationKey(senderSKString, receiverPK); - const encryptedRumour = encrypt(JSON.stringify(rumour), conversationKey); - - const created = randomTimestamp(); - const sealEvent: any = { - kind: 13, // seal - created_at: created, - tags: [], - pubkey: getPublicKey(senderSK), - content: encryptedRumour - } - if (expiryDays != null) { - const expiration = created + (expiryDays + 2) * 86400; - sealEvent.tags.push(["expiration", expiration.toString()]); - } - - return finalizeEvent(sealEvent, senderSK); -} - -const sealRumourNip07 = async (rumour: any, receiverPK: string, expiryDays: number | null = null) => { - const encryptedRumour = await (window as any).nostr.nip44.encrypt( - receiverPK, JSON.stringify(rumour)); + const rumour: any = { + kind: 14, // rumour + created_at: Math.floor(Date.now() / 1000), + tags: [["p", receiverPK]], + pubkey: senderPK, + content: text, + }; + rumour.id = getEventHash(rumour); // do NOT sign rumours! + return rumour; +}; - const created = randomTimestamp(); - const sealEvent: any = { - kind: 13, // seal - created_at: created, - tags: [], - content: encryptedRumour - } - if (expiryDays != null) { - const expiration = created + (expiryDays + 2) * 86400; - sealEvent.tags.push(["expiration", expiration.toString()]); - } +const sealRumourSK = ( + rumour: any, + senderSK: Uint8Array, + receiverPK: string, + expiryDays: number | null = null, +) => { + const senderSKString = bytesToHex(senderSK); + const conversationKey = u.getConversationKey(senderSKString, receiverPK); + const encryptedRumour = encrypt(JSON.stringify(rumour), conversationKey); + + const created = randomTimestamp(); + const sealEvent: any = { + kind: 13, // seal + created_at: created, + tags: [], + pubkey: getPublicKey(senderSK), + content: encryptedRumour, + }; + if (expiryDays != null) { + const expiration = created + (expiryDays + 2) * 86400; + sealEvent.tags.push(["expiration", expiration.toString()]); + } + + return finalizeEvent(sealEvent, senderSK); +}; - return (window as any).nostr.signEvent(sealEvent); -} - -const giftWrap = (event: any, receiverPK: string, expiryDays: number | null = null) => { - const secretKey = generateSecretKey(); - const conversationKey = u.getConversationKey(bytesToHex(secretKey), receiverPK); - const encryptedEvent = encrypt(JSON.stringify(event), conversationKey); - const created = randomTimestamp(); - - const giftWrapEvent: any = { - kind: 1059, // gift wrap - created_at: created, - tags: [["p", receiverPK]], - pubkey: getPublicKey(secretKey), - content: encryptedEvent - } - if (expiryDays != null) { - const expiration = created + (expiryDays + 2) * 86400; - giftWrapEvent.tags.push(["expiration", expiration.toString()]); - } +const sealRumourNip07 = async ( + rumour: any, + receiverPK: string, + expiryDays: number | null = null, +) => { + const encryptedRumour = await (window as any).nostr.nip44.encrypt( + receiverPK, + JSON.stringify(rumour), + ); + + const created = randomTimestamp(); + const sealEvent: any = { + kind: 13, // seal + created_at: created, + tags: [], + content: encryptedRumour, + }; + if (expiryDays != null) { + const expiration = created + (expiryDays + 2) * 86400; + sealEvent.tags.push(["expiration", expiration.toString()]); + } + + return (window as any).nostr.signEvent(sealEvent); +}; - return finalizeEvent(giftWrapEvent, secretKey); -} +const giftWrap = ( + event: any, + receiverPK: string, + expiryDays: number | null = null, +) => { + const secretKey = generateSecretKey(); + const conversationKey = u.getConversationKey( + bytesToHex(secretKey), + receiverPK, + ); + const encryptedEvent = encrypt(JSON.stringify(event), conversationKey); + const created = randomTimestamp(); + + const giftWrapEvent: any = { + kind: 1059, // gift wrap + created_at: created, + tags: [["p", receiverPK]], + pubkey: getPublicKey(secretKey), + content: encryptedEvent, + }; + if (expiryDays != null) { + const expiration = created + (expiryDays + 2) * 86400; + giftWrapEvent.tags.push(["expiration", expiration.toString()]); + } + + return finalizeEvent(giftWrapEvent, secretKey); +}; const decryptNIP17MessageNIP07 = async (wrapped: any): Promise => { - const sealedText = await (window as any).nostr.nip44.decrypt( - wrapped.pubkey, wrapped.content); - const sealed = JSON.parse(sealedText); - const rumourText = await (window as any).nostr.nip44.decrypt( - sealed.pubkey, sealed.content); - return JSON.parse(rumourText); -} + const sealedText = await (window as any).nostr.nip44.decrypt( + wrapped.pubkey, + wrapped.content, + ); + const sealed = JSON.parse(sealedText); + const rumourText = await (window as any).nostr.nip44.decrypt( + sealed.pubkey, + sealed.content, + ); + return JSON.parse(rumourText); +}; // Decrypts the provided message, addressed to the user. // If NIP-07 is available, it uses that, otherwise it triggers the signer modal. export const decrypt = async (wrapped: any, user: any): Promise => { - const sk = await ensureSigner(user.pubkey); + const sk = await ensureSigner(user.pubkey); - if (sk === null) { - return decryptNIP17MessageNIP07(wrapped); - } else { - return unwrapEvent(wrapped, sk); - } -} + if (sk === null) { + return decryptNIP17MessageNIP07(wrapped); + } else { + return unwrapEvent(wrapped, sk); + } +}; // Returns the rumours of the messages the provided user has sent or received. // If the events have an expiration timestamp, // that timestamp is added to its rumour object as the property `expiration`. export const getMessageRumours = async (user: any): Promise => { - const preferredRelays = await getPreferredRelays(user.pubkey); - // Always include default relays to catch messages that might be there - const relays = Array.from(new Set([...preferredRelays, ...DM_RELAYS_LIST])); - const wrapped = await pool.querySync( - relays, { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT } - ); + const preferredRelays = await getPreferredRelays(user.pubkey); + // Always include default relays to catch messages that might be there + const relays = Array.from(new Set([...preferredRelays, ...DM_RELAYS_LIST])); + const wrapped = await pool.querySync(relays, { + kinds: [1059], + "#p": [user.pubkey], + limit: DM_FETCH_LIMIT, + }); + + let rumours: any[] = []; + // intentionally decrypting sequentially to avoid having a bunch of popups + for (const event of wrapped) { + if (expired(event)) { + console.log("relay(s) contain expired event", event.id); + continue; + } + + try { + const rumour: any = await decrypt(event, user); + + const expires = expiration(event); + if (expires) { + rumour.expiration = expires; + } - let rumours: any[] = []; - // intentionally decrypting sequentially to avoid having a bunch of popups - for (const event of wrapped) { - if (expired(event)) { - console.log("relay(s) contain expired event", event.id); - continue; - } - - try { - const rumour: any = await decrypt(event, user); - - const expires = expiration(event); - if (expires) { - rumour.expiration = expires; - } - - rumours.push(rumour); - } catch (e) { - console.error('Failed to decrypt event:', event.id, e); - } + rumours.push(rumour); + } catch (e) { + console.error("Failed to decrypt event:", event.id, e); } + } - return rumours; -} + return rumours; +}; // Returns the preferred relays of the user with the provided pubkey, // according to their kind-10050 event. export const getPreferredRelays = async (pubkey: string): Promise => { - if (relayListCache.has(pubkey)) - return relayListCache.get(pubkey); - - const events = await pool.querySync( - DM_RELAYS_LIST, { kinds: [10050], limit: 1, authors: [pubkey] } - ); - - let relays: string[] = []; - for (const event of events) { - for (const tag of event.tags) { - if (tag.length >= 2 && tag[0] == "relay") { - relays.push(tag[1]); - } - } + if (relayListCache.has(pubkey)) return relayListCache.get(pubkey); + + const events = await pool.querySync(DM_RELAYS_LIST, { + kinds: [10050], + limit: 1, + authors: [pubkey], + }); + + let relays: string[] = []; + for (const event of events) { + for (const tag of event.tags) { + if (tag.length >= 2 && tag[0] == "relay") { + relays.push(tag[1]); + } } + } - // Fall back to default DM relays if user hasn't published their preferred relays - if (relays.length === 0) { - relays = DM_RELAYS_LIST; - } + // Fall back to default DM relays if user hasn't published their preferred relays + if (relays.length === 0) { + relays = DM_RELAYS_LIST; + } - relayListCache.set(pubkey, relays); - return relays; -} + relayListCache.set(pubkey, relays); + return relays; +}; // Publish to a single relay with NIP-42 auth support -const publishWithAuth = async (relayUrl: string, event: any, sk: Uint8Array | null): Promise => { - const relay = await Relay.connect(relayUrl); - - const signAuthEvent = async (authEvent: any) => { - if (sk) { - return finalizeEvent(authEvent, sk); - } else if ((window as any).nostr) { - return await (window as any).nostr.signEvent(authEvent); - } - throw new Error('No signing method for auth'); - }; - - try { - return await relay.publish(event); - } catch (e: any) { - // Check if it's an auth error - if (e?.message?.includes('auth-required')) { - // Authenticate and retry - await relay.auth(signAuthEvent); - return await relay.publish(event); - } - throw e; +const publishWithAuth = async ( + relayUrl: string, + event: any, + sk: Uint8Array | null, +): Promise => { + const relay = await Relay.connect(relayUrl); + + const signAuthEvent = async (authEvent: any) => { + if (sk) { + return finalizeEvent(authEvent, sk); + } else if ((window as any).nostr) { + return await (window as any).nostr.signEvent(authEvent); } + throw new Error("No signing method for auth"); + }; + + try { + return await relay.publish(event); + } catch (e: any) { + // Check if it's an auth error + if (e?.message?.includes("auth-required")) { + // Authenticate and retry + await relay.auth(signAuthEvent); + return await relay.publish(event); + } + throw e; + } }; // Store the current sk for use in publish let currentSk: Uint8Array | null = null; -const publishToPreferred = async (event: any, pubkey: string, expiryEnabled: boolean): Promise => { - let preferredRelays = await getPreferredRelays(pubkey); - if (expiryEnabled) { - preferredRelays = await relaysSupporting(preferredRelays, [40]); - } - if (preferredRelays.length === 0) { - throw new Error('No relays available to publish to'); - } - - const promises = preferredRelays.map(url => publishWithAuth(url, event, currentSk)); - try { - return await (Promise as any).any(promises); - } catch (e) { - const results = await Promise.allSettled(promises); - console.error('All relay publishes failed:', results); - throw e; - } -} +const publishToPreferred = async ( + event: any, + pubkey: string, + expiryEnabled: boolean, +): Promise => { + let preferredRelays = await getPreferredRelays(pubkey); + if (expiryEnabled) { + preferredRelays = await relaysSupporting(preferredRelays, [40]); + } + if (preferredRelays.length === 0) { + throw new Error("No relays available to publish to"); + } + + const promises = preferredRelays.map((url) => + publishWithAuth(url, event, currentSk), + ); + try { + return await (Promise as any).any(promises); + } catch (e) { + const results = await Promise.allSettled(promises); + console.error("All relay publishes failed:", results); + throw e; + } +}; // Sends a NIP-17 message from `user` to `recipient`. // If expiryDays is set, messages will expire after that many days. -export const send = async (message: string, user: any, recipient: any, expiryDays?: number) => { - let event1, event2; - const sk = await ensureSigner(user.pubkey); - - // Store sk for auth handler - currentSk = sk; - - if (sk === null) { - // Use NIP-07 extension - event1 = await createNIP17MessageNIP07( - message, user.pubkey, recipient.pubkey, recipient.pubkey, expiryDays); - event2 = await createNIP17MessageNIP07( - message, user.pubkey, recipient.pubkey, user.pubkey, expiryDays); - } else { - event1 = createNIP17MessageSK( - message, sk, recipient.pubkey, recipient.pubkey, expiryDays); - event2 = createNIP17MessageSK( - message, sk, recipient.pubkey, user.pubkey, expiryDays); - } +export const send = async ( + message: string, + user: any, + recipient: any, + expiryDays?: number, +) => { + let event1, event2; + const sk = await ensureSigner(user.pubkey); + + // Store sk for auth handler + currentSk = sk; + + if (sk === null) { + // Use NIP-07 extension + event1 = await createNIP17MessageNIP07( + message, + user.pubkey, + recipient.pubkey, + recipient.pubkey, + expiryDays, + ); + event2 = await createNIP17MessageNIP07( + message, + user.pubkey, + recipient.pubkey, + user.pubkey, + expiryDays, + ); + } else { + event1 = createNIP17MessageSK( + message, + sk, + recipient.pubkey, + recipient.pubkey, + expiryDays, + ); + event2 = createNIP17MessageSK( + message, + sk, + recipient.pubkey, + user.pubkey, + expiryDays, + ); + } - const p1 = publishToPreferred(event1, recipient.pubkey, expiryDays != null); - const p2 = publishToPreferred(event2, user.pubkey, expiryDays != null); - return Promise.all([p1, p2]); -} + const p1 = publishToPreferred(event1, recipient.pubkey, expiryDays != null); + const p2 = publishToPreferred(event2, user.pubkey, expiryDays != null); + return Promise.all([p1, p2]); +}; diff --git a/src/lib/nip40.ts b/src/lib/nip40.ts index 030fe0df..760a6680 100644 --- a/src/lib/nip40.ts +++ b/src/lib/nip40.ts @@ -1,16 +1,16 @@ // Returns true if the message has an expiration in the past, false otherwise. export const expired = (event: object): boolean => { - const expiryTime = expiration(event); - return expiryTime !== null && Date.now() / 1000 >= expiryTime; -} + const expiryTime = expiration(event); + return expiryTime !== null && Date.now() / 1000 >= expiryTime; +}; // Returns the provided event's expiration time, as a Unix ms integer. // If it has none, returns null. export const expiration = (event: object): number | null => { - for (const tag of event.tags) { - if (tag[0] == "expiration") { - return parseInt(tag[1]); - } + for (const tag of event.tags) { + if (tag[0] == "expiration") { + return parseInt(tag[1]); } - return null; -} + } + return null; +}; diff --git a/src/lib/nip44.ts b/src/lib/nip44.ts index bd7efdfc..e05fc685 100644 --- a/src/lib/nip44.ts +++ b/src/lib/nip44.ts @@ -1,135 +1,163 @@ -// Copied from this public-domain code: -// https://github.com/paulmillr/nip44/blob/main/javascript/index.ts -// Modified slightly to deal with a dependency issue - -import { chacha20 } from '@noble/ciphers/chacha.js'; -import { equalBytes } from '@noble/ciphers/utils.js'; -import { secp256k1 } from '@noble/curves/secp256k1.js'; -import { extract as hkdf_extract, expand as hkdf_expand } from '@noble/hashes/hkdf.js'; -import { hmac } from '@noble/hashes/hmac.js'; -import { sha256 } from '@noble/hashes/sha2.js'; -import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils.js'; -import { base64 } from '@scure/base'; - -declare const TextDecoder: any; - -const decoder = new TextDecoder(); -export const u = { - minPlaintextSize: 0x0001, // 1b msg => padded to 32b - maxPlaintextSize: 0xffff, // 65535 (64kb-1) => padded to 64kb - - utf8Encode: utf8ToBytes, - utf8Decode(bytes: Uint8Array) { - return decoder.decode(bytes); - }, - - getConversationKey(privkeyA: string, pubkeyB: string): Uint8Array { - const sharedX = secp256k1.getSharedSecret(privkeyA, '02' + pubkeyB).subarray(1, 33); - return hkdf_extract(sha256, sharedX, 'nip44-v2'); - }, - - getMessageKeys(conversationKey: Uint8Array, nonce: Uint8Array) { - // ensureBytes(conversationKey, 32); - // ensureBytes(nonce, 32); - const keys = hkdf_expand(sha256, conversationKey, nonce, 76); - return { - chacha_key: keys.subarray(0, 32), - chacha_nonce: keys.subarray(32, 44), - hmac_key: keys.subarray(44, 76), - }; - }, - - calcPaddedLen(len: number): number { - if (!Number.isSafeInteger(len) || len < 1) throw new Error('expected positive integer'); - if (len <= 32) return 32; - const nextPower = 1 << (Math.floor(Math.log2(len - 1)) + 1); - const chunk = nextPower <= 256 ? 32 : nextPower / 8; - return chunk * (Math.floor((len - 1) / chunk) + 1); - }, - - writeU16BE(num: number) { - if (!Number.isSafeInteger(num) || num < u.minPlaintextSize || num > u.maxPlaintextSize) - throw new Error('invalid plaintext size: must be between 1 and 65535 bytes'); - const arr = new Uint8Array(2); - new DataView(arr.buffer).setUint16(0, num, false); - return arr; - }, - - pad(plaintext: string): Uint8Array { - const unpadded = u.utf8Encode(plaintext); - const unpaddedLen = unpadded.length; - const prefix = u.writeU16BE(unpaddedLen); - const suffix = new Uint8Array(u.calcPaddedLen(unpaddedLen) - unpaddedLen); - return concatBytes(prefix, unpadded, suffix); - }, - - unpad(padded: Uint8Array): string { - const unpaddedLen = new DataView(padded.buffer).getUint16(0); - const unpadded = padded.subarray(2, 2 + unpaddedLen); - if ( - unpaddedLen < u.minPlaintextSize || - unpaddedLen > u.maxPlaintextSize || - unpadded.length !== unpaddedLen || - padded.length !== 2 + u.calcPaddedLen(unpaddedLen) - ) - throw new Error('invalid padding'); - return u.utf8Decode(unpadded); - }, - - hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array) { - if (aad.length !== 32) throw new Error('AAD associated data must be 32 bytes'); - const combined = concatBytes(aad, message); - return hmac(sha256, key, combined); - }, - - // metadata: always 65b (version: 1b, nonce: 32b, max: 32b) - // plaintext: 1b to 0xffff - // padded plaintext: 32b to 0xffff - // ciphertext: 32b+2 to 0xffff+2 - // raw payload: 99 (65+32+2) to 65603 (65+0xffff+2) - // compressed payload (base64): 132b to 87472b - decodePayload(payload: string) { - if (typeof payload !== 'string') throw new Error('payload must be a valid string'); - const plen = payload.length; - if (plen < 132 || plen > 87472) throw new Error('invalid payload length: ' + plen); - if (payload[0] === '#') throw new Error('unknown encryption version'); - let data: Uint8Array; - try { - data = base64.decode(payload); - } catch (error) { - throw new Error('invalid base64: ' + (error as any).message); - } - const dlen = data.length; - if (dlen < 99 || dlen > 65603) throw new Error('invalid data length: ' + dlen); - const vers = data[0]; - if (vers !== 2) throw new Error('unknown encryption version ' + vers); - return { - nonce: data.subarray(1, 33), - ciphertext: data.subarray(33, -32), - mac: data.subarray(-32), - }; - }, -}; - -export function encrypt(plaintext: string, conversationKey: Uint8Array, nonce = randomBytes(32)): string { - const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(conversationKey, nonce); - const padded = u.pad(plaintext); - const ciphertext = chacha20(chacha_key, chacha_nonce, padded); - const mac = u.hmacAad(hmac_key, ciphertext, nonce); - return base64.encode(concatBytes(new Uint8Array([2]), nonce, ciphertext, mac)); -} - -export function decrypt(payload: string, conversationKey: Uint8Array): string { - const { nonce, ciphertext, mac } = u.decodePayload(payload); - const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(conversationKey, nonce); - const calculatedMac = u.hmacAad(hmac_key, ciphertext, nonce); - if (!equalBytes(calculatedMac, mac)) throw new Error('invalid MAC'); - const padded = chacha20(chacha_key, chacha_nonce, ciphertext); - return u.unpad(padded); -} - -export const v2 = { - utils: u, - encrypt, - decrypt, -}; +// Copied from this public-domain code: +// https://github.com/paulmillr/nip44/blob/main/javascript/index.ts +// Modified slightly to deal with a dependency issue + +import { chacha20 } from "@noble/ciphers/chacha.js"; +import { equalBytes } from "@noble/ciphers/utils.js"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { + extract as hkdf_extract, + expand as hkdf_expand, +} from "@noble/hashes/hkdf.js"; +import { hmac } from "@noble/hashes/hmac.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { concatBytes, randomBytes, utf8ToBytes } from "@noble/hashes/utils.js"; +import { base64 } from "@scure/base"; + +declare const TextDecoder: any; + +const decoder = new TextDecoder(); +export const u = { + minPlaintextSize: 0x0001, // 1b msg => padded to 32b + maxPlaintextSize: 0xffff, // 65535 (64kb-1) => padded to 64kb + + utf8Encode: utf8ToBytes, + utf8Decode(bytes: Uint8Array) { + return decoder.decode(bytes); + }, + + getConversationKey(privkeyA: string, pubkeyB: string): Uint8Array { + const sharedX = secp256k1 + .getSharedSecret(privkeyA, "02" + pubkeyB) + .subarray(1, 33); + return hkdf_extract(sha256, sharedX, "nip44-v2"); + }, + + getMessageKeys(conversationKey: Uint8Array, nonce: Uint8Array) { + // ensureBytes(conversationKey, 32); + // ensureBytes(nonce, 32); + const keys = hkdf_expand(sha256, conversationKey, nonce, 76); + return { + chacha_key: keys.subarray(0, 32), + chacha_nonce: keys.subarray(32, 44), + hmac_key: keys.subarray(44, 76), + }; + }, + + calcPaddedLen(len: number): number { + if (!Number.isSafeInteger(len) || len < 1) + throw new Error("expected positive integer"); + if (len <= 32) return 32; + const nextPower = 1 << (Math.floor(Math.log2(len - 1)) + 1); + const chunk = nextPower <= 256 ? 32 : nextPower / 8; + return chunk * (Math.floor((len - 1) / chunk) + 1); + }, + + writeU16BE(num: number) { + if ( + !Number.isSafeInteger(num) || + num < u.minPlaintextSize || + num > u.maxPlaintextSize + ) + throw new Error( + "invalid plaintext size: must be between 1 and 65535 bytes", + ); + const arr = new Uint8Array(2); + new DataView(arr.buffer).setUint16(0, num, false); + return arr; + }, + + pad(plaintext: string): Uint8Array { + const unpadded = u.utf8Encode(plaintext); + const unpaddedLen = unpadded.length; + const prefix = u.writeU16BE(unpaddedLen); + const suffix = new Uint8Array(u.calcPaddedLen(unpaddedLen) - unpaddedLen); + return concatBytes(prefix, unpadded, suffix); + }, + + unpad(padded: Uint8Array): string { + const unpaddedLen = new DataView(padded.buffer).getUint16(0); + const unpadded = padded.subarray(2, 2 + unpaddedLen); + if ( + unpaddedLen < u.minPlaintextSize || + unpaddedLen > u.maxPlaintextSize || + unpadded.length !== unpaddedLen || + padded.length !== 2 + u.calcPaddedLen(unpaddedLen) + ) + throw new Error("invalid padding"); + return u.utf8Decode(unpadded); + }, + + hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array) { + if (aad.length !== 32) + throw new Error("AAD associated data must be 32 bytes"); + const combined = concatBytes(aad, message); + return hmac(sha256, key, combined); + }, + + // metadata: always 65b (version: 1b, nonce: 32b, max: 32b) + // plaintext: 1b to 0xffff + // padded plaintext: 32b to 0xffff + // ciphertext: 32b+2 to 0xffff+2 + // raw payload: 99 (65+32+2) to 65603 (65+0xffff+2) + // compressed payload (base64): 132b to 87472b + decodePayload(payload: string) { + if (typeof payload !== "string") + throw new Error("payload must be a valid string"); + const plen = payload.length; + if (plen < 132 || plen > 87472) + throw new Error("invalid payload length: " + plen); + if (payload[0] === "#") throw new Error("unknown encryption version"); + let data: Uint8Array; + try { + data = base64.decode(payload); + } catch (error) { + throw new Error("invalid base64: " + (error as any).message); + } + const dlen = data.length; + if (dlen < 99 || dlen > 65603) + throw new Error("invalid data length: " + dlen); + const vers = data[0]; + if (vers !== 2) throw new Error("unknown encryption version " + vers); + return { + nonce: data.subarray(1, 33), + ciphertext: data.subarray(33, -32), + mac: data.subarray(-32), + }; + }, +}; + +export function encrypt( + plaintext: string, + conversationKey: Uint8Array, + nonce = randomBytes(32), +): string { + const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys( + conversationKey, + nonce, + ); + const padded = u.pad(plaintext); + const ciphertext = chacha20(chacha_key, chacha_nonce, padded); + const mac = u.hmacAad(hmac_key, ciphertext, nonce); + return base64.encode( + concatBytes(new Uint8Array([2]), nonce, ciphertext, mac), + ); +} + +export function decrypt(payload: string, conversationKey: Uint8Array): string { + const { nonce, ciphertext, mac } = u.decodePayload(payload); + const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys( + conversationKey, + nonce, + ); + const calculatedMac = u.hmacAad(hmac_key, ciphertext, nonce); + if (!equalBytes(calculatedMac, mac)) throw new Error("invalid MAC"); + const padded = chacha20(chacha_key, chacha_nonce, ciphertext); + return u.unpad(padded); +} + +export const v2 = { + utils: u, + encrypt, + decrypt, +}; diff --git a/src/lib/nip51.ts b/src/lib/nip51.ts index 0210bf8b..7560dd3d 100644 --- a/src/lib/nip51.ts +++ b/src/lib/nip51.ts @@ -1,70 +1,81 @@ -import { SimplePool } from 'nostr-tools/pool'; -import { signEvent, encrypt, decrypt } from '$lib/nip07'; +import { SimplePool } from "nostr-tools/pool"; +import { signEvent, encrypt, decrypt } from "$lib/nip07"; const pool = new SimplePool(); -import { PUBLIC_DM_RELAYS } from '$env/static/public'; -const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); +import { PUBLIC_DM_RELAYS } from "$env/static/public"; +const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(","); // Mute the provided pubkey, as the provided user. export const mute = async (user: any, pubkey: string, hide: boolean) => { - let { shown, hidden } = await getMuteLists(user); - if (hide) { - hidden.push(["p", pubkey]); - } else { - shown.push(["p", pubkey]); - } - await publishMuteList(user, shown, hidden); -} + let { shown, hidden } = await getMuteLists(user); + if (hide) { + hidden.push(["p", pubkey]); + } else { + shown.push(["p", pubkey]); + } + await publishMuteList(user, shown, hidden); +}; const tagMatches = (tag: string[], pubkey: string): boolean => - tag.length >= 2 && tag[0] === "p" && tag[1] === pubkey; + tag.length >= 2 && tag[0] === "p" && tag[1] === pubkey; // Unmute the provided pubkey, as the provided user. export const unmute = async (user: any, pubkey: string) => { - let { shown, hidden } = await getMuteLists(user); - shown = shown.filter((tag: string[]) => !tagMatches(tag, pubkey)); - hidden = hidden.filter((tag: string[]) => !tagMatches(tag, pubkey)); - await publishMuteList(user, shown, hidden); -} + let { shown, hidden } = await getMuteLists(user); + shown = shown.filter((tag: string[]) => !tagMatches(tag, pubkey)); + hidden = hidden.filter((tag: string[]) => !tagMatches(tag, pubkey)); + await publishMuteList(user, shown, hidden); +}; // Returns a set of the provided user's muted accounts. export const mutedAccounts = async (user: any): Promise> => { - const { shown, hidden } = await getMuteLists(user); - let muted = new Set(); - for (const tag of shown) { - if (tag.length >= 2 && tag[0] === "p") { - muted.add(tag[1]); - } + const { shown, hidden } = await getMuteLists(user); + let muted = new Set(); + for (const tag of shown) { + if (tag.length >= 2 && tag[0] === "p") { + muted.add(tag[1]); } - for (const tag of hidden) { - if (tag.length >= 2 && tag[0] === "p") { - muted.add(tag[1]); - } + } + for (const tag of hidden) { + if (tag.length >= 2 && tag[0] === "p") { + muted.add(tag[1]); } - return muted; -} + } + return muted; +}; const getMuteLists = async (user: any): Promise => { - const listEvents = await pool.querySync( - DM_RELAYS_LIST, { kinds: [10000], "authors": [user.pubkey], limit: 1 } - ); - if (listEvents.length == 0) { - return { shown: [], hidden: [] }; - } + const listEvents = await pool.querySync(DM_RELAYS_LIST, { + kinds: [10000], + authors: [user.pubkey], + limit: 1, + }); + if (listEvents.length == 0) { + return { shown: [], hidden: [] }; + } - return { - shown: listEvents[0].tags, - hidden: listEvents[0].content == "" ? [] : - await decrypt(listEvents[0].content, user, user.pubkey) - }; -} + return { + shown: listEvents[0].tags, + hidden: + listEvents[0].content == "" + ? [] + : await decrypt(listEvents[0].content, user, user.pubkey), + }; +}; -const publishMuteList = async (user: any, shown: string[], hidden: string[]) => { - const event = await signEvent({ - kind: 10000, - created_at: Math.floor(Date.now() / 1000), - tags: shown, - content: await encrypt(hidden, user, user.pubkey) - }, user); - return Promise.any(pool.publish(DM_RELAYS_LIST, event)); -} +const publishMuteList = async ( + user: any, + shown: string[], + hidden: string[], +) => { + const event = await signEvent( + { + kind: 10000, + created_at: Math.floor(Date.now() / 1000), + tags: shown, + content: await encrypt(hidden, user, user.pubkey), + }, + user, + ); + return Promise.any(pool.publish(DM_RELAYS_LIST, event)); +}; diff --git a/src/lib/nostr.ts b/src/lib/nostr.ts index dd283987..50df6408 100644 --- a/src/lib/nostr.ts +++ b/src/lib/nostr.ts @@ -1,239 +1,239 @@ import { Buffer } from "buffer"; import { browser } from "$app/environment"; import { - decrypted, - eventToSign, - password as pw, - passwordPrompt, - signer as $signer, + decrypted, + eventToSign, + password as pw, + passwordPrompt, + signer as $signer, } from "$lib/store"; import { post, wait } from "$lib/utils"; import { bytesToHex } from "@noble/hashes/utils.js"; import type { EventTemplate } from "nostr-tools"; import { get } from "svelte/store"; -let nostrToolsPromise: - | Promise - | undefined; +let nostrToolsPromise: Promise | undefined; let nip44Promise: Promise | undefined; let relayPromise: Promise | undefined; let nip49Promise: Promise | undefined; -const loadNostrTools = () => - (nostrToolsPromise ||= import("nostr-tools")); +const loadNostrTools = () => (nostrToolsPromise ||= import("nostr-tools")); const loadNip44 = () => (nip44Promise ||= import("nostr-tools/nip44")); const loadRelay = () => (relayPromise ||= import("nostr-tools/relay")); const loadNip49 = () => (nip49Promise ||= import("nostr-tools/nip49")); type User = { - [key: string]: any; + [key: string]: any; }; type EncryptParams = { - message: string; - recipient: string; - user: User; + message: string; + recipient: string; + user: User; }; export const encrypt = async ({ message, recipient, user }: EncryptParams) => { - const sk = await getPrivateKey(user); - const { nip04 } = await loadNostrTools(); - return nip04.encrypt(sk, recipient, message); + const sk = await getPrivateKey(user); + const { nip04 } = await loadNostrTools(); + return nip04.encrypt(sk, recipient, message); }; export const decrypt = async ({ event, user }: { event: any; user: any }) => { - const cache = get(decrypted); - try { - let { content, pubkey, id } = event; - if (cache[id]) return cache[id]; - if (pubkey === user.pubkey) pubkey = event.tags[0][1]; - - const { nip04 } = await loadNostrTools(); - const message = await nip04.decrypt( - await getPrivateKey(user), - pubkey, - content, - ); - - cache[id] = Buffer.from(message).toString("utf8"); - decrypted.set(cache); - - return cache[id]; - } catch (e) { - // console.log(e); - } + const cache = get(decrypted); + try { + let { content, pubkey, id } = event; + if (cache[id]) return cache[id]; + if (pubkey === user.pubkey) pubkey = event.tags[0][1]; + + const { nip04 } = await loadNostrTools(); + const message = await nip04.decrypt( + await getPrivateKey(user), + pubkey, + content, + ); + + cache[id] = Buffer.from(message).toString("utf8"); + decrypted.set(cache); + + return cache[id]; + } catch { + // console.log(e); + } }; export const getPrivateKey = async (user: User): Promise => { - let k; - if (browser) { - k = localStorage.getItem("nsec"); - if (k) { - const { nip19 } = await loadNostrTools(); - return nip19.decode(k).data as Uint8Array; - } - } - - const { nsec } = user; - - if (nsec) { - const { decrypt: nip49decrypt } = await loadNip49(); - k = nip49decrypt(nsec, await getPassword()); - } else { - throw new Error("nsec not available"); - } - - const { nip19 } = await loadNostrTools(); - localStorage.setItem("nsec", nip19.nsecEncode(k)); - return k; + let k; + if (browser) { + k = localStorage.getItem("nsec"); + if (k) { + const { nip19 } = await loadNostrTools(); + return nip19.decode(k).data as Uint8Array; + } + } + + const { nsec } = user; + + if (nsec) { + const { decrypt: nip49decrypt } = await loadNip49(); + k = nip49decrypt(nsec, await getPassword()); + } else { + throw new Error("nsec not available"); + } + + const { nip19 } = await loadNostrTools(); + localStorage.setItem("nsec", nip19.nsecEncode(k)); + return k; }; const decodeNsec = async (nsec: string): Promise => { - const { nip19 } = await loadNostrTools(); - const { type, data } = nip19.decode(nsec); - if (type === "nsec" && data instanceof Uint8Array) return data; - throw new Error("invalid nsec"); + const { nip19 } = await loadNostrTools(); + const { type, data } = nip19.decode(nsec); + if (type === "nsec" && data instanceof Uint8Array) return data; + throw new Error("invalid nsec"); }; export const getNsec = async (user: User) => { - const { nip19 } = await loadNostrTools(); - return nip19.nsecEncode(await getPrivateKey(user)); + const { nip19 } = await loadNostrTools(); + return nip19.nsecEncode(await getPrivateKey(user)); }; export const setNsec = async (user: User, nsec: string) => { - const { getPublicKey } = await loadNostrTools(); - user.pubkey = getPublicKey(await decodeNsec(nsec)); - user.nsec = await encryptNsec(nsec); + const { getPublicKey } = await loadNostrTools(); + user.pubkey = getPublicKey(await decodeNsec(nsec)); + user.nsec = await encryptNsec(nsec); }; export const encryptNsec = async (nsec: string) => { - const { encrypt: nip49encrypt } = await loadNip49(); - const d = await decodeNsec(nsec); - return nip49encrypt(d, await getPassword()); + const { encrypt: nip49encrypt } = await loadNip49(); + const d = await decodeNsec(nsec); + return nip49encrypt(d, await getPassword()); }; export const sign = async (event: any, user?: any) => { - if (user?.nsec && !get($signer)?.ready) { - const sk = await getPrivateKey(user); - const { getPublicKey } = await loadNostrTools(); - - $signer.set({ - method: "nsec", - ready: true, - params: { sk: bytesToHex(sk), pk: getPublicKey(sk) }, - }); - } - - eventToSign.set(event); - let unsubscribe; - const { method, params } = await new Promise((r, j) => { - unsubscribe = $signer.subscribe((v) => { - if (v === "cancel") { - j("cancelled"); - $signer.set(undefined); - } - v?.ready && r(v); - }); - }); - await unsubscribe(); - const signedEvent = await signingMethods[method](event, params); - eventToSign.set(signedEvent); - setTimeout(() => eventToSign.set(null), 1000); - return signedEvent; + if (user?.nsec && !get($signer)?.ready) { + const sk = await getPrivateKey(user); + const { getPublicKey } = await loadNostrTools(); + + $signer.set({ + method: "nsec", + ready: true, + params: { sk: bytesToHex(sk), pk: getPublicKey(sk) }, + }); + } + + eventToSign.set(event); + let unsubscribe; + const { method, params } = await new Promise((r, j) => { + unsubscribe = $signer.subscribe((v) => { + if (v === "cancel") { + j("cancelled"); + $signer.set(undefined); + } + v?.ready && r(v); + }); + }); + await unsubscribe(); + const signedEvent = await signingMethods[method](event, params); + eventToSign.set(signedEvent); + setTimeout(() => eventToSign.set(null), 1000); + return signedEvent; }; export const send = (event: EventTemplate) => { - return post("/events", { event }); + return post("/events", { event }); }; const getPassword = async (): Promise => { - if (!get(pw)) passwordPrompt.set(true); - await wait(() => !!get(pw)); - return get(pw) || ""; + if (!get(pw)) passwordPrompt.set(true); + await wait(() => !!get(pw)); + return get(pw) || ""; }; export const nostrConnectRelay = "wss://relay.nsec.app"; -const signingMethods: Record Promise> = { - async connect(event: any, { sk, pk, pubkey }: any) { - const id = crypto.randomUUID(); - const signEvent = { - id, - method: "sign_event", - params: [JSON.stringify(event)], - }; - - const { nip04, finalizeEvent } = await loadNostrTools(); - const content = await nip04.encrypt(sk, pk, JSON.stringify(signEvent)); - - const signedSignEvent = finalizeEvent( - { - created_at: Math.round(Date.now() / 1000), - kind: 24133, - pubkey, - content, - tags: [["p", pk]], - }, - sk, - ); - - const { Relay } = await loadRelay(); - const relay = await Relay.connect(nostrConnectRelay); - relay.publish(signedSignEvent); - - return new Promise((resolve) => { - relay.subscribe([{ kinds: [24133], "#p": [pubkey] }], { - async onevent(event) { - pk = event.pubkey; - try { - let response; - try { - const { nip04 } = await loadNostrTools(); - response = JSON.parse(await nip04.decrypt(sk, pk, event.content)); - } catch (e) { - const { getConversationKey, decrypt: nip44decrypt } = - await loadNip44(); - const ck = await getConversationKey(sk, pk); - response = JSON.parse(await nip44decrypt(event.content, ck)); - } - - if (response.id === id) resolve(JSON.parse(response.result)); - } catch (e) { - console.log("failed to parse nostr connect event", e); - } - }, - }); - }); - }, - - async nsec(event: any, { sk }: any) { - const { finalizeEvent } = await loadNostrTools(); - return finalizeEvent(event, sk); - }, - - async extension(event: any) { - return window.nostr!.signEvent(event); - }, - - async signer(event: any, { pubkey, sig }: any) { - event.pubkey = pubkey; - const { getEventHash } = await loadNostrTools(); - const signedEvent = { - ...event, - id: getEventHash(event), - sig, - }; - return signedEvent; - }, +const signingMethods: Record< + string, + (event: any, params: any) => Promise +> = { + async connect(event: any, { sk, pk, pubkey }: any) { + const id = crypto.randomUUID(); + const signEvent = { + id, + method: "sign_event", + params: [JSON.stringify(event)], + }; + + const { nip04, finalizeEvent } = await loadNostrTools(); + const content = await nip04.encrypt(sk, pk, JSON.stringify(signEvent)); + + const signedSignEvent = finalizeEvent( + { + created_at: Math.round(Date.now() / 1000), + kind: 24133, + pubkey, + content, + tags: [["p", pk]], + }, + sk, + ); + + const { Relay } = await loadRelay(); + const relay = await Relay.connect(nostrConnectRelay); + relay.publish(signedSignEvent); + + return new Promise((resolve) => { + relay.subscribe([{ kinds: [24133], "#p": [pubkey] }], { + async onevent(event) { + pk = event.pubkey; + try { + let response; + try { + const { nip04 } = await loadNostrTools(); + response = JSON.parse(await nip04.decrypt(sk, pk, event.content)); + } catch { + const { getConversationKey, decrypt: nip44decrypt } = + await loadNip44(); + const ck = await getConversationKey(sk, pk); + response = JSON.parse(await nip44decrypt(event.content, ck)); + } + + if (response.id === id) resolve(JSON.parse(response.result)); + } catch (e) { + console.log("failed to parse nostr connect event", e); + } + }, + }); + }); + }, + + async nsec(event: any, { sk }: any) { + const { finalizeEvent } = await loadNostrTools(); + return finalizeEvent(event, sk); + }, + + async extension(event: any) { + return window.nostr!.signEvent(event); + }, + + async signer(event: any, { pubkey, sig }: any) { + event.pubkey = pubkey; + const { getEventHash } = await loadNostrTools(); + const signedEvent = { + ...event, + id: getEventHash(event), + sig, + }; + return signedEvent; + }, }; // Returns all the keys mentioned in the provided event's 'p' tags. export const pTagKeys = (event: any): string[] => { - let keys: string[] = []; - for (const tag of event.tags) { - if (tag[0] === "p") { - keys.push(tag[1]); - } - } - return keys; -} + let keys: string[] = []; + for (const tag of event.tags) { + if (tag[0] === "p") { + keys.push(tag[1]); + } + } + return keys; +}; diff --git a/src/lib/parse.ts b/src/lib/parse.ts index 3249b9db..38c32a54 100644 --- a/src/lib/parse.ts +++ b/src/lib/parse.ts @@ -4,137 +4,137 @@ import { auth, get, isLiquid, post, sats } from "$lib/utils"; import { redirect } from "@sveltejs/kit"; let validatePromise: - | Promise - | undefined; + | Promise + | undefined; const loadValidate = () => - (validatePromise ||= import("bitcoin-address-validation")); + (validatePromise ||= import("bitcoin-address-validation")); export default async (s, host, cookies) => { - if (!s) return; - let t = s.trim(); - let amount; - let invoice; - let user; - - if (t.startsWith("ark") || t.startsWith("tark")) - redirect(307, `/send/ark/${t}`); - - if (t.startsWith("http")) redirect(307, t); - if (t.startsWith(host)) redirect(307, `http://${t}`); - - if (t.includes("lightning=")) { - const url = new URL(t); - const params = new URLSearchParams(url.search); - t = params.get("lightning"); - } - - if (t.startsWith("lightning:")) t = t.replace("lightning:", ""); - if (t.endsWith(`@${PUBLIC_DOMAIN}`)) t = t.split("@")[0]; - if (t.includes("@") && t.includes(".")) { - try { - t = await get(`/encode?address=${t}`); - } catch (e) {} - } - - if (t.toLowerCase().startsWith("nostr:")) { - t = t.split(":")[1]; - } - - if (["note", "nevent"].some((p) => t.startsWith(p))) redirect(307, `/e/${t}`); - if (["nprofile", "npub"].some((p) => t.startsWith(p))) redirect(307, `/${t}`); - - // bitcoin - if ( - t.toLowerCase().startsWith("bitcoin:") || - t.toLowerCase().startsWith("liquidnetwork:") - ) { - ({ - address: t, - options: { amount }, - } = decode(t)); - } - - const { validate } = await loadValidate(); - if (validate(t) || isLiquid(t)) { - let r = `/send/bitcoin/${t}`; - if (amount) r += `/${Math.round(amount * sats)}`; - redirect(307, r); - } - - // lightning - if (t.toLowerCase().startsWith("lightning:")) - t = t.toLowerCase().replace("lightning:", ""); - if (t.toLowerCase().startsWith("lnurl")) redirect(307, `/ln/${t}`); - if (t.includes(":")) t = t.split(":")[1]; - - if (t.toLowerCase().startsWith("lnr")) { - const { iid } = await post("/sendinvoice", { invreq: t }, auth(cookies)); - invoice = { id: iid }; - } else if (t.toLowerCase().startsWith("ln")) { - try { - const aid = cookies.get("aid"); - invoice = await get(`/invoice/${t}`); - if (invoice.user.username === "mint") throw new Error("mint payment"); - if (aid && aid !== invoice.aid) throw new Error("cross-account"); - } catch (e) { - if (t.toLowerCase().startsWith("lno")) { - const { offer_amount_msat: a } = await get(`/decode/${t}`); - if (a) ({ invoice: t } = await post("/fetchinvoice", { offer: t })); - } - redirect(307, `/send/lightning/${t}`); - } - } - - if (invoice) redirect(307, `/invoice/${invoice.id}`); - - if (t.match(/^🥜([\uFE00-\uFE0F]|[\uE0100-\uE01EF]+)$/)) { - t = Array.from(t) - .slice(1) - .map((char) => { - const codePoint = char.codePointAt(0); - - // Handle Variation Selectors (VS1-VS16): U+FE00 to U+FE0F - if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) { - const byteValue = codePoint - 0xfe00; // Maps FE00->0, FE01->1, ..., FE0F->15 - return String.fromCharCode(byteValue); - } - - // Handle Variation Selectors Supplement (VS17-VS256): U+E0100 to U+E01EF - if (codePoint >= 0xe0100 && codePoint <= 0xe01ef) { - const byteValue = codePoint - 0xe0100 + 16; // Maps E0100->16, E0101->17, ..., E01EF->255 - return String.fromCharCode(byteValue); - } - }) - .join(""); - } - - if (t.startsWith("cashu")) { - const { id } = await post("/cash", { token: t }); - redirect(307, `/ecash/${id}`); - } - - if (t.startsWith("creq")) redirect(307, `/send/ecash/${t}`); - - // user - try { - user = await get(`/users/${t.split("/")[0]}`); - if (user.anon) user = null; - } catch (e) {} - - if (user) redirect(307, `/pay/${t}`); - - let fund; - if (t.includes("/fund")) redirect(307, t.substring(t.indexOf("/fund"))); - - try { - fund = await get(`/fund/${t}`); - } catch (e) {} - - if (fund) redirect(307, `/send/fund/${t}`); - - try { - invoice ||= await get(`/invoice/${t}`); - } catch (e) {} - - if (invoice) redirect(307, `/invoice/${invoice.id}`); + if (!s) return; + let t = s.trim(); + let amount; + let invoice; + let user; + + if (t.startsWith("ark") || t.startsWith("tark")) + redirect(307, `/send/ark/${t}`); + + if (t.startsWith("http")) redirect(307, t); + if (t.startsWith(host)) redirect(307, `http://${t}`); + + if (t.includes("lightning=")) { + const url = new URL(t); + const params = new URLSearchParams(url.search); + t = params.get("lightning"); + } + + if (t.startsWith("lightning:")) t = t.replace("lightning:", ""); + if (t.endsWith(`@${PUBLIC_DOMAIN}`)) t = t.split("@")[0]; + if (t.includes("@") && t.includes(".")) { + try { + t = await get(`/encode?address=${t}`); + } catch {} + } + + if (t.toLowerCase().startsWith("nostr:")) { + t = t.split(":")[1]; + } + + if (["note", "nevent"].some((p) => t.startsWith(p))) redirect(307, `/e/${t}`); + if (["nprofile", "npub"].some((p) => t.startsWith(p))) redirect(307, `/${t}`); + + // bitcoin + if ( + t.toLowerCase().startsWith("bitcoin:") || + t.toLowerCase().startsWith("liquidnetwork:") + ) { + ({ + address: t, + options: { amount }, + } = decode(t)); + } + + const { validate } = await loadValidate(); + if (validate(t) || isLiquid(t)) { + let r = `/send/bitcoin/${t}`; + if (amount) r += `/${Math.round(amount * sats)}`; + redirect(307, r); + } + + // lightning + if (t.toLowerCase().startsWith("lightning:")) + t = t.toLowerCase().replace("lightning:", ""); + if (t.toLowerCase().startsWith("lnurl")) redirect(307, `/ln/${t}`); + if (t.includes(":")) t = t.split(":")[1]; + + if (t.toLowerCase().startsWith("lnr")) { + const { iid } = await post("/sendinvoice", { invreq: t }, auth(cookies)); + invoice = { id: iid }; + } else if (t.toLowerCase().startsWith("ln")) { + try { + const aid = cookies.get("aid"); + invoice = await get(`/invoice/${t}`); + if (invoice.user.username === "mint") throw new Error("mint payment"); + if (aid && aid !== invoice.aid) throw new Error("cross-account"); + } catch { + if (t.toLowerCase().startsWith("lno")) { + const { offer_amount_msat: a } = await get(`/decode/${t}`); + if (a) ({ invoice: t } = await post("/fetchinvoice", { offer: t })); + } + redirect(307, `/send/lightning/${t}`); + } + } + + if (invoice) redirect(307, `/invoice/${invoice.id}`); + + if (t.match(/^🥜([\uFE00-\uFE0F]|[\uE0100-\uE01EF]+)$/)) { + t = Array.from(t) + .slice(1) + .map((char) => { + const codePoint = char.codePointAt(0); + + // Handle Variation Selectors (VS1-VS16): U+FE00 to U+FE0F + if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) { + const byteValue = codePoint - 0xfe00; // Maps FE00->0, FE01->1, ..., FE0F->15 + return String.fromCharCode(byteValue); + } + + // Handle Variation Selectors Supplement (VS17-VS256): U+E0100 to U+E01EF + if (codePoint >= 0xe0100 && codePoint <= 0xe01ef) { + const byteValue = codePoint - 0xe0100 + 16; // Maps E0100->16, E0101->17, ..., E01EF->255 + return String.fromCharCode(byteValue); + } + }) + .join(""); + } + + if (t.startsWith("cashu")) { + const { id } = await post("/cash", { token: t }); + redirect(307, `/ecash/${id}`); + } + + if (t.startsWith("creq")) redirect(307, `/send/ecash/${t}`); + + // user + try { + user = await get(`/users/${t.split("/")[0]}`); + if (user.anon) user = null; + } catch {} + + if (user) redirect(307, `/pay/${t}`); + + let fund; + if (t.includes("/fund")) redirect(307, t.substring(t.indexOf("/fund"))); + + try { + fund = await get(`/fund/${t}`); + } catch {} + + if (fund) redirect(307, `/send/fund/${t}`); + + try { + invoice ||= await get(`/invoice/${t}`); + } catch {} + + if (invoice) redirect(307, `/invoice/${invoice.id}`); }; diff --git a/src/lib/passwordCache.ts b/src/lib/passwordCache.ts index 4f764698..a67113e3 100644 --- a/src/lib/passwordCache.ts +++ b/src/lib/passwordCache.ts @@ -5,57 +5,62 @@ const PASSWORD_KEY = "walletPassword"; const EXPIRES_KEY = "walletPasswordExpiresAt"; export const rememberForOptions = [ - { label: "Don't remember", ms: 0 }, - { label: "8 hours", ms: 8 * 60 * 60 * 1000 }, - { label: "1 day", ms: 24 * 60 * 60 * 1000 }, - { label: "1 week", ms: 7 * 24 * 60 * 60 * 1000 }, - { label: "1 month", ms: 30 * 24 * 60 * 60 * 1000 }, - { label: "1 year", ms: 365 * 24 * 60 * 60 * 1000 }, + { label: "Don't remember", ms: 0 }, + { label: "8 hours", ms: 8 * 60 * 60 * 1000 }, + { label: "1 day", ms: 24 * 60 * 60 * 1000 }, + { label: "1 week", ms: 7 * 24 * 60 * 60 * 1000 }, + { label: "1 month", ms: 30 * 24 * 60 * 60 * 1000 }, + { label: "1 year", ms: 365 * 24 * 60 * 60 * 1000 }, ]; export const defaultRememberForMs = 30 * 24 * 60 * 60 * 1000; const clearStoredPassword = () => { - if (!browser) return; - localStorage.removeItem(PASSWORD_KEY); - localStorage.removeItem(EXPIRES_KEY); + if (!browser) return; + localStorage.removeItem(PASSWORD_KEY); + localStorage.removeItem(EXPIRES_KEY); }; const loadStoredPassword = () => { - if (!browser) return undefined; - const password = localStorage.getItem(PASSWORD_KEY) || undefined; - if (!password) return undefined; - const expiresAtRaw = localStorage.getItem(EXPIRES_KEY); - const expiresAt = expiresAtRaw ? Number(expiresAtRaw) : undefined; - if (expiresAt && Date.now() > expiresAt) { - clearStoredPassword(); - return undefined; - } - return password; + if (!browser) return undefined; + const password = localStorage.getItem(PASSWORD_KEY) || undefined; + if (!password) return undefined; + const expiresAtRaw = localStorage.getItem(EXPIRES_KEY); + const expiresAt = expiresAtRaw ? Number(expiresAtRaw) : undefined; + if (expiresAt && Date.now() > expiresAt) { + clearStoredPassword(); + return undefined; + } + return password; }; -export const walletPassword = writable(loadStoredPassword()); - -export const rememberWalletPassword = (password: string, durationMs: number) => { - if (!browser) return; - if (!durationMs) { - clearStoredPassword(); - walletPassword.set(undefined); - return; - } - const expiresAt = Date.now() + durationMs; - localStorage.setItem(PASSWORD_KEY, password); - localStorage.setItem(EXPIRES_KEY, String(expiresAt)); - walletPassword.set(password); +export const walletPassword = writable( + loadStoredPassword(), +); + +export const rememberWalletPassword = ( + password: string, + durationMs: number, +) => { + if (!browser) return; + if (!durationMs) { + clearStoredPassword(); + walletPassword.set(undefined); + return; + } + const expiresAt = Date.now() + durationMs; + localStorage.setItem(PASSWORD_KEY, password); + localStorage.setItem(EXPIRES_KEY, String(expiresAt)); + walletPassword.set(password); }; export const forgetWalletPassword = () => { - clearStoredPassword(); - walletPassword.set(undefined); + clearStoredPassword(); + walletPassword.set(undefined); }; export const getRememberedWalletPassword = () => { - const password = loadStoredPassword(); - walletPassword.set(password); - return password; + const password = loadStoredPassword(); + walletPassword.set(password); + return password; }; diff --git a/src/lib/punks.ts b/src/lib/punks.ts index 8b7b53df..e5551062 100644 --- a/src/lib/punks.ts +++ b/src/lib/punks.ts @@ -1,66 +1,66 @@ export default [ - "6e1f1626a9308abd0b7732fecbc7d49366ced46dead63ef3603fb0f4ec7aa53d", - "96ea95735b61c48c6f465db8ca0246a62539f34c3a977be6338770d92461da89", - "8c18c036b257d7b9c5a970a786efa04c320f3cf5afa73fa310a6603744d1ee3a", - "a7a6187e54e83acdb893d456c8b4ad6877e4738ea9e0d47c9b2070bf2c98a35f", - "836d72150b6f3033b6220d15ac9d49457d81041a700d12898bd3b8e737b4d265", - "e3d1c40538a8aed282a430647e3ff16ff94b64b8ed0a7cf427e8b0e2780a9b2a", - "2155224ef35cfb4bbdb06d982b42bb9b93d97b2bc691c22c804e1759a0a04882", - "5f6549ad9dd15dd57c204b1a9b60c68c376e77d7d114cfa13219e6a1a71f901f", - "ba8dd5a7070aee7102d4f8bc83ac497091e2e473a0f448a1c513569167dd0791", - "4f07e1da3685a858970a9f39300564971e97257f5d37b55a6d9882014aecc421", - "01aa9348244c4b65de3df758d49c98076e17a109aef04bc211abc77cf42dc51d", - "080574eb9e0613926e0e5d9b667c132e12965a1b1d47218a0b290a2d55c55d2f", - "08ce4840184f63a779bcd1f91bbcb56a2e8cba75c3ba2dbd290a559a57e4238e", - "27caa38c14a85ad64b4c4469a34e61a3965a1cbc4cad9a85707da374301c830d", - "eccc90741370ba4acc70bc65f4ccc38fddb1327d977c366156018974bc52fe4a", - "3759946d9e875ac2fc5c51ed850686193bb1e0716219e42efd52f3965993b275", - "db5fe28b8ddb9ae5e4f5e0e757ee60bc938bffbdfdd38de807939e212d3c5066", - "3f8f861e663accee51e68b987705ce647a7446b7f118abc94c632e158acbb44f", - "43bab95a047112dca6705506f916a97252363a20da4a4c0a5ab7b676e11083fc", - "d3018e45c66dd485697f738ad191af98267e6255d562496dd06b1a662e0c7a88", - "0ec2e8d07e053d686140857493ece653cde52764a60aa1d25651d81811d44a3d", - "4595bb092682af7177799464364c047fbfa7ede767336734949bce5d085fce15", - "6524936709e0719f9e28bf1caac91f86f323d1a79f10ab65a03fa9ea2e6b3270", - "8ba4736edc68da8e5b21c23af4de1f9131f47778c2a5b2a970a799223a9380b3", - "2aaba839e8546a8cb6d04a81ff5be83ac4e700ca4491607a16ebd60dc4995750", - "5f88f7f579ce50d7bcf60d6e9556f77acf361ba54caf3eb61dc4f2ca9c94c6a2", - "d1ca262272613d7386fecce7f1ad268188940b59ea83fdd1f8d7ab85771e3f43", - "5211890e4128d8d730802cc75ee105867e8878581d5e72ffc98a0d60027a5398", - "c1dececcbd7b623bfaa726392512e6ea110db93a0649dd01bf9e559318514f7c", - "43ce93c9835e983403a01329918d184832c5516e1b0ea4e0878b4c4def042777", - "c84724a398bda8d20436fb0663d059c196628fa71192bb720fa5adf6137bed9b", - "f307620a90a36045fa550316d61be4755c1f6c7f3fc81c95fc4d590ac568b2c4", - "78ae0118856a1f17c13f292628fcf3dccc150c7278578caaceb9f35ca8bc9f55", - "9d9a9a40c490f05d7dbafde8bf60d68aab7dcb79a7976eef62d1448a4549816b", - "f6017bec50563dbbb24b979214ae225f96b01dea5b090b174038ff2842a1c6b1", - "6dbec748ea87e959390b0df429c005fccffe9e672864f555c14c7a407b940870", - "11701e76233395ebdba8d1d50334d1a0cc3a158f3452260c9a7b9a067b78d753", - "7354f3829dd05c838aa16d66b00316f7994797927a4d3b594c8af04a2b4ae86a", - "14d254333c6240fbdacbbbe261f7ae15be2e83bffcf084f4f59f459bae0a4571", - "7849ac0f31056265efb0110038f86e61effe17b7db836377d88433b73312e65a", - "9018be885177c675baf9b36978428d841ca30a9185e11de1a31238a08e3ec1af", - "777c5715de7423597fea6c03c496ea46414f0508cd96e119261f002965beb805", - "b742b7509a97c2b346ea762f0510e8475b30dc988417f0f146ae00a800c6dd56", - "7af3029a826e6dfba23ad502901a3632ef4ee047bd45d618b1f106717b0af7ea", - "82390020b2bca461204cc49b20ee6fec1b93dad476726f377785d04040a4d6c3", - "6bc2990699be7f610c9f6a49bc7a19b034040727bc3de63fee89a1573fe00673", - "50c7c2e8e8c7f4aabffb195187a1ac1105f5712859f02d491d794ba2d8498e08", - "5d1d3769cd141603ce907df5dca159e389ca4d75338eddbe1b7db1c0766cbfd6", - "4d8ad7a761d068b7ef9b058fb4ba924b276e7a45a3ad47a79b767e08e821e038", - "d5b5b2167602f7277cdac76cfda8bdad0befcb8ff12df495dfabf40c91d1120c", - "6f2618272bb0164247e9b0f40b19f020c1c9253510dc658a3cc078d079ef01bf", - "fd502d5007bfc62a91eb7a34c1ae23b9de4a20cf5e1bbd86dadc421d7083099c", - "914813583b901044970b9c7d6dcdfde4344339325f5988d0dd681d377f1243fb", - "85cb7cf0245613d08b3027f879f58a6db645bf0db107242beb856b340ee49ee8", - "d6dbb18d5df5e92c8c74ee59aa86d19dff25a5d0f9d030e84285a686f86804d7", - "65309f82cd9f9926a578d09e1ad13909f68a06d02b853760731a4225f7aecef4", - "433519d13f2b64fe82a90cd054797d4779507fea93c1054bd51c35ba846a0f7f", - "62a55d4d6e4b6f992a1c1840f041585c957affcdbf875202cf9e1db9def05aeb", - "e19eb1eeabdeffa04ccd792eb09776690cf2121887b26a7c5d20d66e3e8fd0fb", - "a80951fc593035abb2dda045a2076d29a30f5cc470e0a20db39c3ec588ed61d8", - "4f1b39906714266a7a1936bfa8e08c5d891285dd2b188b4bcd4afb0107671004", - "f46310b2e60727af071d09e759814f52f933688a0ff0995c7da373b381e70005", - "2cc94821f56d21918ba4f2770d97f280e06fd99f4199167e2f4ddc780f404c8a", - "698a2ca7a09e88fa6594801da037f4dbce6ae841d1d33736aebcf8f6fe104e01", + "6e1f1626a9308abd0b7732fecbc7d49366ced46dead63ef3603fb0f4ec7aa53d", + "96ea95735b61c48c6f465db8ca0246a62539f34c3a977be6338770d92461da89", + "8c18c036b257d7b9c5a970a786efa04c320f3cf5afa73fa310a6603744d1ee3a", + "a7a6187e54e83acdb893d456c8b4ad6877e4738ea9e0d47c9b2070bf2c98a35f", + "836d72150b6f3033b6220d15ac9d49457d81041a700d12898bd3b8e737b4d265", + "e3d1c40538a8aed282a430647e3ff16ff94b64b8ed0a7cf427e8b0e2780a9b2a", + "2155224ef35cfb4bbdb06d982b42bb9b93d97b2bc691c22c804e1759a0a04882", + "5f6549ad9dd15dd57c204b1a9b60c68c376e77d7d114cfa13219e6a1a71f901f", + "ba8dd5a7070aee7102d4f8bc83ac497091e2e473a0f448a1c513569167dd0791", + "4f07e1da3685a858970a9f39300564971e97257f5d37b55a6d9882014aecc421", + "01aa9348244c4b65de3df758d49c98076e17a109aef04bc211abc77cf42dc51d", + "080574eb9e0613926e0e5d9b667c132e12965a1b1d47218a0b290a2d55c55d2f", + "08ce4840184f63a779bcd1f91bbcb56a2e8cba75c3ba2dbd290a559a57e4238e", + "27caa38c14a85ad64b4c4469a34e61a3965a1cbc4cad9a85707da374301c830d", + "eccc90741370ba4acc70bc65f4ccc38fddb1327d977c366156018974bc52fe4a", + "3759946d9e875ac2fc5c51ed850686193bb1e0716219e42efd52f3965993b275", + "db5fe28b8ddb9ae5e4f5e0e757ee60bc938bffbdfdd38de807939e212d3c5066", + "3f8f861e663accee51e68b987705ce647a7446b7f118abc94c632e158acbb44f", + "43bab95a047112dca6705506f916a97252363a20da4a4c0a5ab7b676e11083fc", + "d3018e45c66dd485697f738ad191af98267e6255d562496dd06b1a662e0c7a88", + "0ec2e8d07e053d686140857493ece653cde52764a60aa1d25651d81811d44a3d", + "4595bb092682af7177799464364c047fbfa7ede767336734949bce5d085fce15", + "6524936709e0719f9e28bf1caac91f86f323d1a79f10ab65a03fa9ea2e6b3270", + "8ba4736edc68da8e5b21c23af4de1f9131f47778c2a5b2a970a799223a9380b3", + "2aaba839e8546a8cb6d04a81ff5be83ac4e700ca4491607a16ebd60dc4995750", + "5f88f7f579ce50d7bcf60d6e9556f77acf361ba54caf3eb61dc4f2ca9c94c6a2", + "d1ca262272613d7386fecce7f1ad268188940b59ea83fdd1f8d7ab85771e3f43", + "5211890e4128d8d730802cc75ee105867e8878581d5e72ffc98a0d60027a5398", + "c1dececcbd7b623bfaa726392512e6ea110db93a0649dd01bf9e559318514f7c", + "43ce93c9835e983403a01329918d184832c5516e1b0ea4e0878b4c4def042777", + "c84724a398bda8d20436fb0663d059c196628fa71192bb720fa5adf6137bed9b", + "f307620a90a36045fa550316d61be4755c1f6c7f3fc81c95fc4d590ac568b2c4", + "78ae0118856a1f17c13f292628fcf3dccc150c7278578caaceb9f35ca8bc9f55", + "9d9a9a40c490f05d7dbafde8bf60d68aab7dcb79a7976eef62d1448a4549816b", + "f6017bec50563dbbb24b979214ae225f96b01dea5b090b174038ff2842a1c6b1", + "6dbec748ea87e959390b0df429c005fccffe9e672864f555c14c7a407b940870", + "11701e76233395ebdba8d1d50334d1a0cc3a158f3452260c9a7b9a067b78d753", + "7354f3829dd05c838aa16d66b00316f7994797927a4d3b594c8af04a2b4ae86a", + "14d254333c6240fbdacbbbe261f7ae15be2e83bffcf084f4f59f459bae0a4571", + "7849ac0f31056265efb0110038f86e61effe17b7db836377d88433b73312e65a", + "9018be885177c675baf9b36978428d841ca30a9185e11de1a31238a08e3ec1af", + "777c5715de7423597fea6c03c496ea46414f0508cd96e119261f002965beb805", + "b742b7509a97c2b346ea762f0510e8475b30dc988417f0f146ae00a800c6dd56", + "7af3029a826e6dfba23ad502901a3632ef4ee047bd45d618b1f106717b0af7ea", + "82390020b2bca461204cc49b20ee6fec1b93dad476726f377785d04040a4d6c3", + "6bc2990699be7f610c9f6a49bc7a19b034040727bc3de63fee89a1573fe00673", + "50c7c2e8e8c7f4aabffb195187a1ac1105f5712859f02d491d794ba2d8498e08", + "5d1d3769cd141603ce907df5dca159e389ca4d75338eddbe1b7db1c0766cbfd6", + "4d8ad7a761d068b7ef9b058fb4ba924b276e7a45a3ad47a79b767e08e821e038", + "d5b5b2167602f7277cdac76cfda8bdad0befcb8ff12df495dfabf40c91d1120c", + "6f2618272bb0164247e9b0f40b19f020c1c9253510dc658a3cc078d079ef01bf", + "fd502d5007bfc62a91eb7a34c1ae23b9de4a20cf5e1bbd86dadc421d7083099c", + "914813583b901044970b9c7d6dcdfde4344339325f5988d0dd681d377f1243fb", + "85cb7cf0245613d08b3027f879f58a6db645bf0db107242beb856b340ee49ee8", + "d6dbb18d5df5e92c8c74ee59aa86d19dff25a5d0f9d030e84285a686f86804d7", + "65309f82cd9f9926a578d09e1ad13909f68a06d02b853760731a4225f7aecef4", + "433519d13f2b64fe82a90cd054797d4779507fea93c1054bd51c35ba846a0f7f", + "62a55d4d6e4b6f992a1c1840f041585c957affcdbf875202cf9e1db9def05aeb", + "e19eb1eeabdeffa04ccd792eb09776690cf2121887b26a7c5d20d66e3e8fd0fb", + "a80951fc593035abb2dda045a2076d29a30f5cc470e0a20db39c3ec588ed61d8", + "4f1b39906714266a7a1936bfa8e08c5d891285dd2b188b4bcd4afb0107671004", + "f46310b2e60727af071d09e759814f52f933688a0ff0995c7da373b381e70005", + "2cc94821f56d21918ba4f2770d97f280e06fd99f4199167e2f4ddc780f404c8a", + "698a2ca7a09e88fa6594801da037f4dbce6ae841d1d33736aebcf8f6fe104e01", ]; diff --git a/src/lib/random.ts b/src/lib/random.ts index 36dde0cc..605471ac 100644 --- a/src/lib/random.ts +++ b/src/lib/random.ts @@ -1,20 +1,20 @@ import { - NumberDictionary, - animals, - colors, - uniqueNamesGenerator, + NumberDictionary, + animals, + colors, + uniqueNamesGenerator, } from "unique-names-generator"; export const randomName = () => - uniqueNamesGenerator({ - dictionaries: [animals, NumberDictionary.generate({ min: 10, max: 99 })], - length: 2, - separator: "", - }); + uniqueNamesGenerator({ + dictionaries: [animals, NumberDictionary.generate({ min: 10, max: 99 })], + length: 2, + separator: "", + }); export const randomPassword = () => - uniqueNamesGenerator({ - dictionaries: [colors, NumberDictionary.generate({ min: 100, max: 999 })], - length: 2, - separator: "", - }); + uniqueNamesGenerator({ + dictionaries: [colors, NumberDictionary.generate({ min: 100, max: 999 })], + length: 2, + separator: "", + }); diff --git a/src/lib/rates.ts b/src/lib/rates.ts index 96aaeac2..5d55550d 100644 --- a/src/lib/rates.ts +++ b/src/lib/rates.ts @@ -4,10 +4,10 @@ let last; let rates; export default async function getRates() { - if (!rates || Date.now() - last > 60000) { - rates = await get("/rates"); - last = Date.now(); - } + if (!rates || Date.now() - last > 60000) { + rates = await get("/rates"); + last = Date.now(); + } - return rates; + return rates; } diff --git a/src/lib/samples.ts b/src/lib/samples.ts index ae7ef6d5..172efca0 100644 --- a/src/lib/samples.ts +++ b/src/lib/samples.ts @@ -1,52 +1,52 @@ import { PUBLIC_COINOS_URL, PUBLIC_DOMAIN } from "$env/static/public"; const api = PUBLIC_DOMAIN.includes("localhost") - ? `${PUBLIC_COINOS_URL}` - : `https://${PUBLIC_DOMAIN}/api`; + ? `${PUBLIC_COINOS_URL}` + : `https://${PUBLIC_DOMAIN}/api`; export default { - me: `curl "${api}/me" -H "content-type: application/json"`, - update: `curl "${api}/user" -H "content-type: application/json" -d '{ + me: `curl "${api}/me" -H "content-type: application/json"`, + update: `curl "${api}/user" -H "content-type: application/json" -d '{ "username": "demo", }'`, - register: `curl "${api}/register" -H "content-type: application/json" -d '{ + register: `curl "${api}/register" -H "content-type: application/json" -d '{ "user": { "username": "demo", "password": "hunter2" } }'`, - login: `curl "${api}/login" -H "content-type: application/json" -d '{ + login: `curl "${api}/login" -H "content-type: application/json" -d '{ "username": "demo", "password": "hunter2" }'`, - payments: `curl "${api}/payments" + payments: `curl "${api}/payments" -H "content-type: application/json" -H "Authorization: Bearer $token" `, - lightning: `curl "${api}/payments" + lightning: `curl "${api}/payments" -H "content-type: application/json" -H "Authorization: Bearer $token" -d '{ "payreq": "lnbc1pj94d8fsp5n77k340ps4m9jn7kp8he8yynpddvurv6mcsrrqpnq5l2jxdxzlwqpp5m96pqhc5nrlk8cqsu9ufdxxa43sarp8vwf9egvm2pg9nl0zu9r8qdq2vdhkjmn0wvxqztgcqpjrzjqwhmav82kntsppmkp8jp4vg4h9nns78tsy8mg7ve4lq5txrkp0h56zlarvqqdtcqqsqqqqqqqqqqp6cq9q9qyysgqzxlypywzphyujm3ga5j5csfcmqvlnae0fgnvymkaaw94eeg7py5rqzysyjkr3ev2snq63qpsc69vf54adkd0szvmvwt5cuadjnuy95sq4xfa40" }'`, - bitcoin: `curl "${api}/bitcoin/send" + bitcoin: `curl "${api}/bitcoin/send" -H "content-type: application/json" -H "Authorization: Bearer $token" -d '{ "amount": 5000, "address": "bc1q3unh97w4rmelflrm2hvdwz37d8kray3vn4d5ca" }'`, - internal: `curl "${api}/send" + internal: `curl "${api}/send" -H "content-type: application/json" -H "Authorization: Bearer $token" -d '{ "amount": 5000, "username": "alice" }'`, - rates: `curl "${api}/rates" + rates: `curl "${api}/rates" -H "content-type: application/json" }'`, - lightningInvoice: `curl "${api}/invoice" + lightningInvoice: `curl "${api}/invoice" -H "content-type: application/json" -H "Authorization: Bearer $token" -d '{ @@ -55,7 +55,7 @@ export default { "type": "lightning" } }'`, - bitcoinAddress: `curl "${api}/invoice" + bitcoinAddress: `curl "${api}/invoice" -H "content-type: application/json" -H "Authorization: Bearer $token" -d '{ @@ -64,7 +64,7 @@ export default { "type": "bitcoin" } }'`, - webhook: `curl "${api}/invoice" + webhook: `curl "${api}/invoice" -H "content-type: application/json" -H "Authorization: Bearer $token" -d '{ @@ -74,9 +74,9 @@ export default { "secret": "webhooksecret" } }'`, - fetchInvoice: `curl "${api}/invoice/bc1qmhfk9stzffhd9umzmld92vff7zg3mdlh7rvvaj" + fetchInvoice: `curl "${api}/invoice/bc1qmhfk9stzffhd9umzmld92vff7zg3mdlh7rvvaj" -H "content-type: application/json"'`, - invoiceResponse: `{ + invoiceResponse: `{ "amount":3141 "tip":0 "type":"bitcoin" @@ -89,5 +89,5 @@ export default { "received":0 "created":1677537428134 }`, - socketAuth: `echo '{"type":"login","data":{"username":"user","password":"password"}}' | websocat -n https://coinos.io/ws`, + socketAuth: `echo '{"type":"login","data":{"username":"user","password":"password"}}' | websocat -n https://coinos.io/ws`, }; diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 6f4d3f62..a3b875c0 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -2,28 +2,28 @@ import { auth, fd, post } from "$lib/utils"; import { fail } from "@sveltejs/kit"; type Error = { - message: string; + message: string; }; export default async ({ cookies, request }) => { - const form = await fd(request); + const form = await fd(request); - if (form.tab === "account") { - form.notify = form.notify === "on"; - form.push = form.push === "on"; - form.nip5 = form.nip5 === "on"; - form.prompt = form.prompt === "on"; - form.autowithdraw = form.autowithdraw === "on"; - } + if (form.tab === "account") { + form.notify = form.notify === "on"; + form.push = form.push === "on"; + form.nip5 = form.nip5 === "on"; + form.prompt = form.prompt === "on"; + form.autowithdraw = form.autowithdraw === "on"; + } - try { - const { user } = await post("/user", form, auth(cookies)); + try { + const { user } = await post("/user", form, auth(cookies)); - if (user.language) cookies.set("lang", user.language, { path: "/" }); + if (user.language) cookies.set("lang", user.language, { path: "/" }); - return { user, success: true }; - } catch (e) { - const { message } = e as Error; - return fail(400, { message }); - } + return { user, success: true }; + } catch (e) { + const { message } = e as Error; + return fail(400, { message }); + } }; diff --git a/src/lib/socket.ts b/src/lib/socket.ts index b87cc165..e40eb39b 100644 --- a/src/lib/socket.ts +++ b/src/lib/socket.ts @@ -12,57 +12,57 @@ let token; export const auth = () => token && send("login", token); export const send = async (type, data) => { - try { - await wait(() => socket?.readyState === 1, 1000, 10); - } catch (e) { - const { message } = e as Error; - if (message === "timeout") reconnectToWebsocket(); - } - - await wait(() => socket?.readyState === 1, 1000, 10); - socket.send(JSON.stringify({ type, data })); + try { + await wait(() => socket?.readyState === 1, 1000, 10); + } catch (e) { + const { message } = e as Error; + if (message === "timeout") reconnectToWebsocket(); + } + + await wait(() => socket?.readyState === 1, 1000, 10); + socket.send(JSON.stringify({ type, data })); }; export const messages = (data) => ({ - id() { - last.set(Date.now()); - }, - invoice() { - invoice.set(data); - }, - event() { - event.set(data); - }, - - request() { - request.set(data); - invalidate("app:invoice"); - }, - - async payment() { - const { amount, confirmed, iid } = data; - invalidate("app:user"); - invalidate("app:invoice"); - invalidate("app:payments"); - - const { - url: { pathname }, - } = get(page); - - const username = cookies.get("username"); - - if (amount > 0) { - if ( - pathname.includes(`/${username}`) || - pathname.includes("/receive") || - pathname.includes("/invoice") - ) { - if (!get(navigating)) goto(`/invoice/${iid}`); - } else success(`${confirmed ? "Received" : "Detected"} ⚡️${s(amount)}!`); - } - }, - - connected: auth, + id() { + last.set(Date.now()); + }, + invoice() { + invoice.set(data); + }, + event() { + event.set(data); + }, + + request() { + request.set(data); + invalidate("app:invoice"); + }, + + async payment() { + const { amount, confirmed, iid } = data; + invalidate("app:user"); + invalidate("app:invoice"); + invalidate("app:payments"); + + const { + url: { pathname }, + } = get(page); + + const username = cookies.get("username"); + + if (amount > 0) { + if ( + pathname.includes(`/${username}`) || + pathname.includes("/receive") || + pathname.includes("/invoice") + ) { + if (!get(navigating)) goto(`/invoice/${iid}`); + } else success(`${confirmed ? "Received" : "Detected"} ⚡️${s(amount)}!`); + } + }, + + connected: auth, }); const initialReconnectDelay = 1000; @@ -72,51 +72,51 @@ let currentReconnectDelay = initialReconnectDelay; let connecting; export function connect(t) { - if (connecting) return; - connecting = true; - setTimeout(() => { - connecting = false; - }, 5000); + if (connecting) return; + connecting = true; + setTimeout(() => { + connecting = false; + }, 5000); - token = t; + token = t; - if (socket) return auth(); + if (socket) return auth(); - socket = new WebSocket(PUBLIC_SOCKET); - socket.addEventListener("open", onWebsocketOpen); - socket.addEventListener("close", onWebsocketClose); - socket.addEventListener("message", onWebsocketMessage); + socket = new WebSocket(PUBLIC_SOCKET); + socket.addEventListener("open", onWebsocketOpen); + socket.addEventListener("close", onWebsocketClose); + socket.addEventListener("message", onWebsocketMessage); } export function close() { - if (socket) socket.close(); + if (socket) socket.close(); } async function onWebsocketMessage(msg) { - const { type, data } = JSON.parse(msg.data); - if (get(navigating)) await sleep(2000); + const { type, data } = JSON.parse(msg.data); + if (get(navigating)) await sleep(2000); - if (messages(data)[type]) messages(data)[type](); + if (messages(data)[type]) messages(data)[type](); } function onWebsocketOpen() { - currentReconnectDelay = initialReconnectDelay; - send("heartbeat", token); + currentReconnectDelay = initialReconnectDelay; + send("heartbeat", token); } function onWebsocketClose() { - socket = null; - setTimeout( - () => { - reconnectToWebsocket(); - }, - currentReconnectDelay + Math.floor(Math.random() * 3000), - ); + socket = null; + setTimeout( + () => { + reconnectToWebsocket(); + }, + currentReconnectDelay + Math.floor(Math.random() * 3000), + ); } function reconnectToWebsocket() { - if (currentReconnectDelay < maxReconnectDelay) { - currentReconnectDelay *= 2; - } - connect(token); + if (currentReconnectDelay < maxReconnectDelay) { + currentReconnectDelay *= 2; + } + connect(token); } diff --git a/src/lib/store.ts b/src/lib/store.ts index 7cd17aec..e14d107f 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -2,73 +2,73 @@ import { browser } from "$app/environment"; import { writable } from "svelte/store"; export const persistSession = (key, defaultValue = undefined) => { - const s = writable( - browser && - sessionStorage.getItem(key) && - sessionStorage.getItem(key) !== "undefined" - ? JSON.parse(sessionStorage.getItem(key) || "") - : defaultValue, - ); + const s = writable( + browser && + sessionStorage.getItem(key) && + sessionStorage.getItem(key) !== "undefined" + ? JSON.parse(sessionStorage.getItem(key) || "") + : defaultValue, + ); - s.subscribe((v) => { - browser && sessionStorage.setItem(key, JSON.stringify(v)); - }); + s.subscribe((v) => { + browser && sessionStorage.setItem(key, JSON.stringify(v)); + }); - return s; + return s; }; export const persistLocal = (key, defaultValue: any = undefined) => { - const s = writable( - browser && - localStorage.getItem(key) && - localStorage.getItem(key) !== "undefined" - ? JSON.parse(localStorage.getItem(key) || "") - : defaultValue, - ); + const s = writable( + browser && + localStorage.getItem(key) && + localStorage.getItem(key) !== "undefined" + ? JSON.parse(localStorage.getItem(key) || "") + : defaultValue, + ); - s.subscribe((v) => { - try { - if (browser) { - if (v) { - localStorage.setItem(key, JSON.stringify(v)); - } else { - localStorage.removeItem(key); - } - } - } catch (e) { - console.log("problem setting key", v); - console.log(e); - } - }); + s.subscribe((v) => { + try { + if (browser) { + if (v) { + localStorage.setItem(key, JSON.stringify(v)); + } else { + localStorage.removeItem(key); + } + } + } catch (e) { + console.log("problem setting key", v); + console.log(e); + } + }); - return s; + return s; }; export const persistLocalValue = (key, defaultValue: any = undefined) => { - const s = writable( - browser && - localStorage.getItem(key) && - localStorage.getItem(key) !== "undefined" - ? JSON.parse(localStorage.getItem(key) || "") - : defaultValue, - ); + const s = writable( + browser && + localStorage.getItem(key) && + localStorage.getItem(key) !== "undefined" + ? JSON.parse(localStorage.getItem(key) || "") + : defaultValue, + ); - s.subscribe((v) => { - try { - if (browser) { - if (v === undefined) { - localStorage.removeItem(key); - } else { - localStorage.setItem(key, JSON.stringify(v)); - } - } - } catch (e) { - console.log("problem setting key", v); - console.log(e); - } - }); + s.subscribe((v) => { + try { + if (browser) { + if (v === undefined) { + localStorage.removeItem(key); + } else { + localStorage.setItem(key, JSON.stringify(v)); + } + } + } catch (e) { + console.log("problem setting key", v); + console.log(e); + } + }); - return s; + return s; }; export const account = writable(); diff --git a/src/lib/ticket.ts b/src/lib/ticket.ts index 0d93f390..48ed1a66 100644 --- a/src/lib/ticket.ts +++ b/src/lib/ticket.ts @@ -3,29 +3,29 @@ import { env } from "$env/dynamic/private"; import tickets from "$lib/tickets"; export const createTicket = async (cookies, username) => { - const { ticket } = await get("/ticket"); + const { ticket } = await get("/ticket"); - const { address } = await post( - `/${username}/invoice`, - { invoice: { network: "liquid" }, user: { username } }, - auth(cookies), - ); + const { address } = await post( + `/${username}/invoice`, + { invoice: { network: "liquid" }, user: { username } }, + auth(cookies), + ); - const { asset } = await post( - "/assets", - { - address, - name: `Launch Party Ticket ${ticket}`, - ticker: "LPT", - precision: 0, - asset_amount: "1", - token_amount: 0, - domain: "coinos.io", - filename: tickets[ticket], - }, - { authorization: `Bearer ${env.LAUNCH}` }, - ); + const { asset } = await post( + "/assets", + { + address, + name: `Launch Party Ticket ${ticket}`, + ticker: "LPT", + precision: 0, + asset_amount: "1", + token_amount: 0, + domain: "coinos.io", + filename: tickets[ticket], + }, + { authorization: `Bearer ${env.LAUNCH}` }, + ); - await post("/ticket", { asset }, { authorization: `Bearer ${env.LAUNCH}` }); - return asset; + await post("/ticket", { asset }, { authorization: `Bearer ${env.LAUNCH}` }); + return asset; }; diff --git a/src/lib/tickets.ts b/src/lib/tickets.ts index f37fad78..1f8cbdac 100644 --- a/src/lib/tickets.ts +++ b/src/lib/tickets.ts @@ -1,52 +1,52 @@ export default [ - "QmPbFkiXNuaQyfwD4W9GcXP7qWaNd6wFd9KCnJir5mes1J", - "QmPVjvbaMxSJRpDxMLYTdUWQbknej1vYLckBQ2pAgxZgJN", - "QmVasycSFzCYhyXrcvTE7mN3d6W78RFnrhzg5cRF6Dp6Zq", - "QmTmnhjpTBjZdeHwXgT9Dftc3uWMW73qsZpZ5ECrTyiKKZ", - "QmWDBiyVPyFkzUrEdvy7DSJe1rBQpene1m6quKAtyjBP6a", - "QmcxxzQZwgG7SE9QbHNvoTiCi6Dhs9rrz9roqvxm2JQA88", - "Qmd9ThEpXLLHimEzRUWEFrAGLSw5PAHzn2SmwfCnjNLwWt", - "Qmc7pWw1yxc2ARBWitDMYiyPkWK61NsXXU8oYAQwNAvcx1", - "QmX9Hgw1rMFUgBBuq8QfZZDzLjN5xCED19fwFdDeDDqwmr", - "QmconCAyNyaagqKvvfKCbG7Eu8c6vCtkv2ivvxMoFN9YwA", - "QmWbFmvViy5gx4JdD5sw5YJETet4ZQcXtYgijYnrsnSZ3a", - "QmP6QiiJwoqkduZMqdd5e5htHZ4aYHCsvEu7WEEUknKYa5", - "QmefHk8hvBUHsuKugntok5u75Q8i886cnPwsj4LZZB6uWg", - "QmdrSDK5ByYHxc88hfDu8hQgK5XYFpXSfPorUXdyTbE437", - "QmZ6ZycjRzYpsjnLm1Lfgk1fXXDdhcYRkyfmqYJZVGwADy", - "QmSkBxS5yLPjB2VPs89oQRZANsoyodK6s8s52E8GKXr2zt", - "QmSn8TCPwq3UZ1Z4eyko76CvQo2H2Hyef1NGboDQN22m5i", - "QmP6CTH5zrKMzZwtjy7VaK9NXiq4Gw53kmuRjUbJjeovoe", - "QmdwT4yvz9Xf8g3t6irKRoh3Wa7MwRQrcUJckQxyQe1Bc5", - "QmcwuHH3bD5JExqjDvKqRQ3V5xEVGqs94qPNVDzaCqJa5a", - "QmRfVduHwn8sXwSoyfgZpxSi8L7EjP4LMp6fmijo9VApDp", - "Qmc4TGj1Ahbg9HJ9WDXDm2bXsNkT4zRDp8Wdw9wqqdEwuH", - "QmY8uQiHMWA8ztf2wAurNFW7eiTHQEUiLTQ2vGceMwVqz3", - "QmXiXbaD12F4mkbUSQcKhWshkPU4oi29cUawZGnPk86VVU", - "QmZaiAGjvBZBw4suz33s9JQNzY2Kshean2mQQPCa6gKMQe", - "Qme8bDSdzX7tanPYf672xXW8vPdkLkeSLqLzHJqidRXsYJ", - "QmRJirjuUKgh2AMyBF4jNquaXHFPBtDJf3gKFC614hX7qu", - "QmdRoSKSgabcrpekNBPB6T47RBtJnCETYE8ry2a3P37MUe", - "QmXoqArruHSghrUGW35vVQHPknBG5Cj939iWWHHXU3B616", - "QmYCxtDuoHLMAFhNpifUsLU9ptAeFKwyxSiSEJhDQLMZtN", - "QmQZQBw8wgzUsrmVDEejdePK23AEJbk9aUracvxLPyHz2x", - "QmSsbu7U3i62AQKuYqfisRo5E6nqdMZfHRKWYJgouaXDxD", - "QmaP4adVQuWyqtN4QCXeYXj4nr19Je4EqsaTG1zM9ywqFS", - "QmPPsvgfXsiN3wNqdAwHAhQVpw6SZWNpry9Na6R2Xb7zXY", - "QmR7z6wQmcfZUvRsVK83GRRboyvwZEDQujLwumMY33nAit", - "QmQXgkgorDLPUyQpFbW8aLUW6jioQjUKamtXw5UuNCttDg", - "QmcwYBeXt1BPfdSrtSrXFpWctRZ7RDHvmKTttC5hPPTSzG", - "QmTUexZnw3xPy3YVdUsqJALwTvy2qFax5b3QpJVMvTCJQb", - "QmNLkYXFUvctpfeeDTZxruZE2f834EUnYMM6JJUeTcUB4J", - "QmT7544UaTPgKFYMCpPE4unXphNGX71BLZBPKLkxAijSmj", - "QmTpVR4dqX1QV4fae5pZ1ACtRcANHwEWCLwpMXFmwE95Sm", - "Qmdfyps5fW8d8wstXMfpvpei2DTmaADBWgmxLT7e7uan4P", - "QmUSK8byipYoG3H97QpmPrHw9vYrvHygyhkiM81XmaKyxS", - "QmQUGRvp96LTw5i8Ly9f5LoMvUpeQHCLGNqC88KV74wEHK", - "Qma693TFLh9Z1MeBFzhPHi6Yk4tmTcZY2jogwJ8cPkU2B4", - "QmNu8FyMpmkFdbCXGRVG8mKJtMvvCoLP8LNzFsxBQwxQeS", - "QmWZHE87PKujL1MGLuFgP3D12o5GQxYbvbeTRgr12f1XMu", - "QmcZJn8gPGd3poS9yuiNJh6iP4cn9uy5fnypky71r4sSSw", - "QmTwrAF4E9ucpdnrUZFj6EfrQamGv7Qyn7ceSx1DUQYceC", - "QmexssxU5iTXypgNqrmrQwMDJnTNxyZUxor5wDLnwb5TZp", + "QmPbFkiXNuaQyfwD4W9GcXP7qWaNd6wFd9KCnJir5mes1J", + "QmPVjvbaMxSJRpDxMLYTdUWQbknej1vYLckBQ2pAgxZgJN", + "QmVasycSFzCYhyXrcvTE7mN3d6W78RFnrhzg5cRF6Dp6Zq", + "QmTmnhjpTBjZdeHwXgT9Dftc3uWMW73qsZpZ5ECrTyiKKZ", + "QmWDBiyVPyFkzUrEdvy7DSJe1rBQpene1m6quKAtyjBP6a", + "QmcxxzQZwgG7SE9QbHNvoTiCi6Dhs9rrz9roqvxm2JQA88", + "Qmd9ThEpXLLHimEzRUWEFrAGLSw5PAHzn2SmwfCnjNLwWt", + "Qmc7pWw1yxc2ARBWitDMYiyPkWK61NsXXU8oYAQwNAvcx1", + "QmX9Hgw1rMFUgBBuq8QfZZDzLjN5xCED19fwFdDeDDqwmr", + "QmconCAyNyaagqKvvfKCbG7Eu8c6vCtkv2ivvxMoFN9YwA", + "QmWbFmvViy5gx4JdD5sw5YJETet4ZQcXtYgijYnrsnSZ3a", + "QmP6QiiJwoqkduZMqdd5e5htHZ4aYHCsvEu7WEEUknKYa5", + "QmefHk8hvBUHsuKugntok5u75Q8i886cnPwsj4LZZB6uWg", + "QmdrSDK5ByYHxc88hfDu8hQgK5XYFpXSfPorUXdyTbE437", + "QmZ6ZycjRzYpsjnLm1Lfgk1fXXDdhcYRkyfmqYJZVGwADy", + "QmSkBxS5yLPjB2VPs89oQRZANsoyodK6s8s52E8GKXr2zt", + "QmSn8TCPwq3UZ1Z4eyko76CvQo2H2Hyef1NGboDQN22m5i", + "QmP6CTH5zrKMzZwtjy7VaK9NXiq4Gw53kmuRjUbJjeovoe", + "QmdwT4yvz9Xf8g3t6irKRoh3Wa7MwRQrcUJckQxyQe1Bc5", + "QmcwuHH3bD5JExqjDvKqRQ3V5xEVGqs94qPNVDzaCqJa5a", + "QmRfVduHwn8sXwSoyfgZpxSi8L7EjP4LMp6fmijo9VApDp", + "Qmc4TGj1Ahbg9HJ9WDXDm2bXsNkT4zRDp8Wdw9wqqdEwuH", + "QmY8uQiHMWA8ztf2wAurNFW7eiTHQEUiLTQ2vGceMwVqz3", + "QmXiXbaD12F4mkbUSQcKhWshkPU4oi29cUawZGnPk86VVU", + "QmZaiAGjvBZBw4suz33s9JQNzY2Kshean2mQQPCa6gKMQe", + "Qme8bDSdzX7tanPYf672xXW8vPdkLkeSLqLzHJqidRXsYJ", + "QmRJirjuUKgh2AMyBF4jNquaXHFPBtDJf3gKFC614hX7qu", + "QmdRoSKSgabcrpekNBPB6T47RBtJnCETYE8ry2a3P37MUe", + "QmXoqArruHSghrUGW35vVQHPknBG5Cj939iWWHHXU3B616", + "QmYCxtDuoHLMAFhNpifUsLU9ptAeFKwyxSiSEJhDQLMZtN", + "QmQZQBw8wgzUsrmVDEejdePK23AEJbk9aUracvxLPyHz2x", + "QmSsbu7U3i62AQKuYqfisRo5E6nqdMZfHRKWYJgouaXDxD", + "QmaP4adVQuWyqtN4QCXeYXj4nr19Je4EqsaTG1zM9ywqFS", + "QmPPsvgfXsiN3wNqdAwHAhQVpw6SZWNpry9Na6R2Xb7zXY", + "QmR7z6wQmcfZUvRsVK83GRRboyvwZEDQujLwumMY33nAit", + "QmQXgkgorDLPUyQpFbW8aLUW6jioQjUKamtXw5UuNCttDg", + "QmcwYBeXt1BPfdSrtSrXFpWctRZ7RDHvmKTttC5hPPTSzG", + "QmTUexZnw3xPy3YVdUsqJALwTvy2qFax5b3QpJVMvTCJQb", + "QmNLkYXFUvctpfeeDTZxruZE2f834EUnYMM6JJUeTcUB4J", + "QmT7544UaTPgKFYMCpPE4unXphNGX71BLZBPKLkxAijSmj", + "QmTpVR4dqX1QV4fae5pZ1ACtRcANHwEWCLwpMXFmwE95Sm", + "Qmdfyps5fW8d8wstXMfpvpei2DTmaADBWgmxLT7e7uan4P", + "QmUSK8byipYoG3H97QpmPrHw9vYrvHygyhkiM81XmaKyxS", + "QmQUGRvp96LTw5i8Ly9f5LoMvUpeQHCLGNqC88KV74wEHK", + "Qma693TFLh9Z1MeBFzhPHi6Yk4tmTcZY2jogwJ8cPkU2B4", + "QmNu8FyMpmkFdbCXGRVG8mKJtMvvCoLP8LNzFsxBQwxQeS", + "QmWZHE87PKujL1MGLuFgP3D12o5GQxYbvbeTRgr12f1XMu", + "QmcZJn8gPGd3poS9yuiNJh6iP4cn9uy5fnypky71r4sSSw", + "QmTwrAF4E9ucpdnrUZFj6EfrQamGv7Qyn7ceSx1DUQYceC", + "QmexssxU5iTXypgNqrmrQwMDJnTNxyZUxor5wDLnwb5TZp", ]; diff --git a/src/lib/types.ts b/src/lib/types.ts index 19277ec1..827f96aa 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,23 +1,23 @@ export interface Message { - author: User; - created_at: number; - recipient: User; + author: User; + created_at: number; + recipient: User; } export interface Note { - created_at: number; - seen: number; + created_at: number; + seen: number; } export interface User { - id: string; + id: string; } export interface Invoice { - aid: string; - id?: string; - memoPrompt?: boolean; - memo?: string; - rate: number; - type?: string; + aid: string; + id?: string; + memoPrompt?: boolean; + memo?: string; + rate: number; + type?: string; } diff --git a/src/lib/upload.ts b/src/lib/upload.ts index 6d9d4230..aaeec007 100644 --- a/src/lib/upload.ts +++ b/src/lib/upload.ts @@ -1,18 +1,18 @@ export const upload = async (file, type, progress) => { - const url = `/api/upload/${type}`; - const formData = new FormData(); - formData.append("file", file); + const url = `/api/upload/${type}`; + const formData = new FormData(); + formData.append("file", file); - return new Promise((resolve, reject) => { - const ajax = new XMLHttpRequest(); - ajax.addEventListener("load", () => { - if (ajax.readyState === ajax.DONE) { - if (ajax.status === 200) resolve(ajax.response); - else reject(new Error(`Upload failed: ${ajax.response}`)); - } - }); - ajax.upload.addEventListener("progress", progress, false); - ajax.open("POST", url); - ajax.send(formData); - }); + return new Promise((resolve, reject) => { + const ajax = new XMLHttpRequest(); + ajax.addEventListener("load", () => { + if (ajax.readyState === ajax.DONE) { + if (ajax.status === 200) resolve(ajax.response); + else reject(new Error(`Upload failed: ${ajax.response}`)); + } + }); + ajax.upload.addEventListener("progress", progress, false); + ajax.open("POST", url); + ajax.send(formData); + }); }; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 45afb90b..ebec6caa 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -6,16 +6,16 @@ import { bytesToHex } from "@noble/hashes/utils.js"; import { fail as svelteFail, redirect } from "@sveltejs/kit"; import { toast } from "@zerodevx/svelte-toast"; import { - differenceInDays, - differenceInHours, - differenceInMinutes, - differenceInSeconds, + differenceInDays, + differenceInHours, + differenceInMinutes, + differenceInSeconds, } from "date-fns"; import { get as getStore } from "svelte/store"; export function scroll(section: any) { - if (getStore(page).url.pathname !== "/") goto("/"); - setTimeout(() => section.scrollIntoView({ behavior: "smooth" }), 500); + if (getStore(page).url.pathname !== "/") goto("/"); + setTimeout(() => section.scrollIntoView({ behavior: "smooth" }), 500); } const base = browser ? "" : PUBLIC_COINOS_URL; @@ -23,523 +23,530 @@ const base = browser ? "" : PUBLIC_COINOS_URL; let getIp: (() => string | undefined) | undefined; export const setIpGetter = (fn: () => string | undefined) => { - getIp = fn; + getIp = fn; }; const ipHeaders = (): Record => { - const ip = getIp?.(); - return ip ? { "cf-connecting-ip": ip } : {}; + const ip = getIp?.(); + return ip ? { "cf-connecting-ip": ip } : {}; }; export const punk = (k: string) => - `${Math.floor((parseInt(k.slice(-2), 16) / 256) * 64) + 1}.webp`; + `${Math.floor((parseInt(k.slice(-2), 16) / 256) * 64) + 1}.webp`; export const g = (url: string, fetch: any, headers: object) => - fetch(base + url, { headers: { ...ipHeaders(), ...headers } }) - .then((r: Response) => r.text()) - .then((body: string) => { - try { - return JSON.parse(body); - } catch (e) { - throw new Error(body); - } - }); + fetch(base + url, { headers: { ...ipHeaders(), ...headers } }) + .then((r: Response) => r.text()) + .then((body: string) => { + try { + return JSON.parse(body); + } catch { + throw new Error(body); + } + }); export const get = (url: string, headers = {}) => { - return fetch(base + url, { headers: { ...ipHeaders(), ...headers } }) - .then(async (r) => { - if (r.ok) return r.text(); - throw new Error(await r.text()); - }) - .then((body) => { - try { - return JSON.parse(body); - } catch (e) { - return body; - } - }); + return fetch(base + url, { headers: { ...ipHeaders(), ...headers } }) + .then(async (r) => { + if (r.ok) return r.text(); + throw new Error(await r.text()); + }) + .then((body) => { + try { + return JSON.parse(body); + } catch { + return body; + } + }); }; export const post = async ( - url: string, - body: object, - headers: HeadersInit | undefined = undefined, + url: string, + body: object, + headers: HeadersInit | undefined = undefined, ) => { - headers = { - "content-type": "application/json", - accept: "application/json", - ...ipHeaders(), - ...headers, - }; - - const response = await fetch(base + url, { - method: "POST", - body: JSON.stringify(body), - headers, - }) - .then(async (r) => { - if (r.ok) return r.text(); - const text = await r.text(); - - let message; - try { - ({ message } = JSON.parse(text)); - } catch (e) {} - - if (message) throw new Error(message); - throw new Error(text); - }) - .then((body) => { - let json; - try { - json = JSON.parse(body); - } catch (e) { - throw new Error(body); - } - - if (json.error instanceof Error) throw json.error; - if (json instanceof Error) throw body; - if (json.name === "Error") throw new Error(json.message); - - return json; - }); - - return response; + headers = { + "content-type": "application/json", + accept: "application/json", + ...ipHeaders(), + ...headers, + }; + + const response = await fetch(base + url, { + method: "POST", + body: JSON.stringify(body), + headers, + }) + .then(async (r) => { + if (r.ok) return r.text(); + const text = await r.text(); + + let message; + try { + ({ message } = JSON.parse(text)); + } catch {} + + if (message) throw new Error(message); + throw new Error(text); + }) + .then((body) => { + let json; + try { + json = JSON.parse(body); + } catch { + throw new Error(body); + } + + if (json.error instanceof Error) throw json.error; + if (json instanceof Error) throw body; + if (json.name === "Error") throw new Error(json.message); + + return json; + }); + + return response; }; export const copy = (text: string) => { - navigator.clipboard.writeText(text); - success("Copied!"); + navigator.clipboard.writeText(text); + success("Copied!"); }; export const copyNoNewlines = (text: string) => { - const stripped = text.replace(/\n/g, " ").replace(/\s+/g, " "); - navigator.clipboard.writeText(stripped); - success("Copied!"); + const stripped = text.replace(/\n/g, " ").replace(/\s+/g, " "); + navigator.clipboard.writeText(stripped); + success("Copied!"); }; let recent: string[] = []; export const success = (m: string, clear = true) => { - if (recent.includes(m)) return; - recent.push(m); - setTimeout(() => (recent = []), 5000); - if (clear) toast.pop(); - toast.push(m, { - theme: { - "--toastBarBackground": "#F5F7FA", - }, - }); + if (recent.includes(m)) return; + recent.push(m); + setTimeout(() => (recent = []), 5000); + if (clear) toast.pop(); + toast.push(m, { + theme: { + "--toastBarBackground": "#F5F7FA", + }, + }); }; export const warning = (m: string, clear = true) => { - if (clear) toast.pop(); - toast.push(m, { - theme: { - "--toastBarBackground": "#FFCE22", - }, - }); + if (clear) toast.pop(); + toast.push(m, { + theme: { + "--toastBarBackground": "#FFCE22", + }, + }); }; export const fail = (m: string, clear = true) => { - if (clear) toast.pop(); - toast.push(m, { - theme: { - "--toastBarBackground": "#E93535", - }, - }); + if (clear) toast.pop(); + toast.push(m, { + theme: { + "--toastBarBackground": "#E93535", + }, + }); }; export const info = (m: string) => { - toast.pop(); - toast.push(m, { - theme: { - "--toastBarBackground": "white", - }, - }); + toast.pop(); + toast.push(m, { + theme: { + "--toastBarBackground": "white", + }, + }); }; export const login = async ( - user: { - username: string; - password: string; - token?: string; - challenge?: string; - recaptcha?: string; - }, - cookies, - ip: string, - host?: string, - extraHeaders?: Record, + user: { + username: string; + password: string; + token?: string; + challenge?: string; + recaptcha?: string; + }, + cookies, + ip: string, + host?: string, + extraHeaders?: Record, ) => { - const maxAge = 380 * 24 * 60 * 60; - - const headers: Record = { - "content-type": "application/json", - accept: "application/json", - "cf-connecting-ip": ip, - ...extraHeaders, - }; - if (host) headers["x-forwarded-host"] = host; - - const res = await fetch(`${base}/login`, { - method: "POST", - body: JSON.stringify(user), - headers, - }); - - const text = await res.text(); - - if (res.status === 401) { - if (text.startsWith("2fa")) throw new Error("2fa"); - throw new Error(text); - } - - const { user: u, token } = JSON.parse(text); - if (!token) throw new Error("Login failed"); - - const expires = new Date(); - expires.setSeconds(expires.getSeconds() + maxAge); - - const opts = { path: "/", expires }; - if (u.language) cookies.set("lang", u.language, opts); - cookies.set("username", user.username, opts); - cookies.set("token", token, opts); - if (u.nsec) { - try { - const { decrypt } = await import("nostr-tools/nip49"); - cookies.set("sk", bytesToHex(decrypt(u.nsec, user.password)), { - ...opts, - httpOnly: false, - sameSite: "lax", - }); - } catch (e) {} - } + const maxAge = 380 * 24 * 60 * 60; + + const headers: Record = { + "content-type": "application/json", + accept: "application/json", + "cf-connecting-ip": ip, + ...extraHeaders, + }; + if (host) headers["x-forwarded-host"] = host; + + const res = await fetch(`${base}/login`, { + method: "POST", + body: JSON.stringify(user), + headers, + }); + + const text = await res.text(); + + if (res.status === 401) { + if (text.startsWith("2fa")) throw new Error("2fa"); + throw new Error(text); + } + + const { user: u, token } = JSON.parse(text); + if (!token) throw new Error("Login failed"); + + const expires = new Date(); + expires.setSeconds(expires.getSeconds() + maxAge); + + const opts = { path: "/", expires }; + if (u.language) cookies.set("lang", u.language, opts); + cookies.set("username", user.username, opts); + cookies.set("token", token, opts); + if (u.nsec) { + try { + const { decrypt } = await import("nostr-tools/nip49"); + cookies.set("sk", bytesToHex(decrypt(u.nsec, user.password)), { + ...opts, + httpOnly: false, + sameSite: "lax", + }); + } catch {} + } }; export const auth = (cookies) => ({ - authorization: `Bearer ${cookies.get("token")}`, + authorization: `Bearer ${cookies.get("token")}`, }); export const isInvalidTokenError = (e: unknown) => { - const message = (e as Error)?.message || ""; - if (!message) return false; - const normalized = message.toLowerCase(); - return ( - normalized.includes("jwt") || - normalized.includes("invalid token") || - normalized.includes("token expired") || - normalized.includes("expired token") || - normalized.includes("unauthorized") || - normalized.includes("not authorized") - ); + const message = (e as Error)?.message || ""; + if (!message) return false; + const normalized = message.toLowerCase(); + return ( + normalized.includes("jwt") || + normalized.includes("invalid token") || + normalized.includes("token expired") || + normalized.includes("expired token") || + normalized.includes("unauthorized") || + normalized.includes("not authorized") + ); }; export const btc = (fiat: number, rate: number): number => - Math.round((fiat * sats) / rate); + Math.round((fiat * sats) / rate); export const toFiat = (amount: number, rate: number): number => - (amount * rate) / sats; + (amount * rate) / sats; export const fd = async (req: Request): Promise => { - const obj: Record = Object.fromEntries(await req.formData()); - for (const k in obj) - (obj[k] === "undefined" && (obj[k] = undefined)) || - (obj[k] === "false" && (obj[k] = false)); - return obj; + const obj: Record = Object.fromEntries(await req.formData()); + for (const k in obj) + (obj[k] === "undefined" && (obj[k] = undefined)) || + (obj[k] === "false" && (obj[k] = false)); + return obj; }; const map = { - en: { - CAD: "en-CA", - USD: "en-US", - GBP: "en-GB", - AUD: "en-AU", - EUR: "en-IE", // Eurozone English (Ireland) - NZD: "en-NZ", - JPY: "en-JP", // English in Japan - INR: "en-IN", // English in India - ZAR: "en-ZA", // South Africa - }, - fr: { - CAD: "fr-CA", // French Canada - EUR: "fr-FR", // France - CHF: "fr-CH", // French Switzerland - XOF: "fr-SN", // Francophone West Africa - }, - de: { - EUR: "de-DE", // Germany - CHF: "de-CH", // German Switzerland - USD: "de-US", // German for the US - }, - es: { - EUR: "es-ES", // Spain - USD: "es-US", // Spanish for the US - MXN: "es-MX", // Mexico - ARS: "es-AR", // Argentina - CLP: "es-CL", // Chile - }, - pt: { - EUR: "pt-PT", // Portugal - BRL: "pt-BR", // Brazil - }, - zh: { - CNY: "zh-CN", // Simplified Chinese (Mainland China) - HKD: "zh-HK", // Traditional Chinese (Hong Kong) - TWD: "zh-TW", // Traditional Chinese (Taiwan) - USD: "zh-US", // Chinese for the US - }, - ja: { - JPY: "ja-JP", // Japan - USD: "ja-US", // Japanese for the US - }, - ru: { - RUB: "ru-RU", // Russia - USD: "ru-US", // Russian for the US - }, - el: { - EUR: "el-GR", // Greece - }, - fa: { - IRR: "fa-IR", // Iran - }, - // Global currency mappings (fallbacks) - CAD: "en-CA", - USD: "en-US", - GBP: "en-GB", - AUD: "en-AU", - EUR: "en-IE", - NZD: "en-NZ", - JPY: "ja-JP", - INR: "en-IN", - ZAR: "en-ZA", - CHF: "de-CH", - MXN: "es-MX", - BRL: "pt-BR", - CNY: "zh-CN", - HKD: "zh-HK", - TWD: "zh-TW", - RUB: "ru-RU", - IRR: "fa-IR", + en: { + CAD: "en-CA", + USD: "en-US", + GBP: "en-GB", + AUD: "en-AU", + EUR: "en-IE", // Eurozone English (Ireland) + NZD: "en-NZ", + JPY: "en-JP", // English in Japan + INR: "en-IN", // English in India + ZAR: "en-ZA", // South Africa + }, + fr: { + CAD: "fr-CA", // French Canada + EUR: "fr-FR", // France + CHF: "fr-CH", // French Switzerland + XOF: "fr-SN", // Francophone West Africa + }, + de: { + EUR: "de-DE", // Germany + CHF: "de-CH", // German Switzerland + USD: "de-US", // German for the US + }, + es: { + EUR: "es-ES", // Spain + USD: "es-US", // Spanish for the US + MXN: "es-MX", // Mexico + ARS: "es-AR", // Argentina + CLP: "es-CL", // Chile + }, + pt: { + EUR: "pt-PT", // Portugal + BRL: "pt-BR", // Brazil + }, + zh: { + CNY: "zh-CN", // Simplified Chinese (Mainland China) + HKD: "zh-HK", // Traditional Chinese (Hong Kong) + TWD: "zh-TW", // Traditional Chinese (Taiwan) + USD: "zh-US", // Chinese for the US + }, + ja: { + JPY: "ja-JP", // Japan + USD: "ja-US", // Japanese for the US + }, + ru: { + RUB: "ru-RU", // Russia + USD: "ru-US", // Russian for the US + }, + el: { + EUR: "el-GR", // Greece + }, + fa: { + IRR: "fa-IR", // Iran + }, + // Global currency mappings (fallbacks) + CAD: "en-CA", + USD: "en-US", + GBP: "en-GB", + AUD: "en-AU", + EUR: "en-IE", + NZD: "en-NZ", + JPY: "ja-JP", + INR: "en-IN", + ZAR: "en-ZA", + CHF: "de-CH", + MXN: "es-MX", + BRL: "pt-BR", + CNY: "zh-CN", + HKD: "zh-HK", + TWD: "zh-TW", + RUB: "ru-RU", + IRR: "fa-IR", }; export const loc = (user) => { - if (!user) return; - const { currency: c, language: l } = user; - return map[l]?.[c] || map[c]; + if (!user) return; + const { currency: c, language: l } = user; + return map[l]?.[c] || map[c]; }; export const f = ( - s: number, - currency: string, - locale = "en-US", - minimumFractionDigits = 2, - maximumFractionDigits = 2, + s: number, + currency: string, + locale = "en-US", + minimumFractionDigits = 2, + maximumFractionDigits = 2, ) => { - const l = typeof locale === "function" ? locale() : locale || "en-US"; - const cur = currency || "USD"; - const region = l.length >= 2 ? l.slice(-2) : ""; - const currencyDisplay = - region && cur.startsWith(region) ? "narrowSymbol" : "symbol"; - return new Intl.NumberFormat(l, { - style: "currency", - currency: cur, - currencyDisplay, - minimumFractionDigits, - maximumFractionDigits, - }).format(s); + const l = typeof locale === "function" ? locale() : locale || "en-US"; + const cur = currency || "USD"; + const region = l.length >= 2 ? l.slice(-2) : ""; + const currencyDisplay = + region && cur.startsWith(region) ? "narrowSymbol" : "symbol"; + return new Intl.NumberFormat(l, { + style: "currency", + currency: cur, + currencyDisplay, + minimumFractionDigits, + maximumFractionDigits, + }).format(s); }; export const s = (s: number, locale = "en-US"): string => { - const l = typeof locale === "function" ? locale() : locale || "en-US"; - return new Intl.NumberFormat(l, { maximumFractionDigits: 0 }).format(s); + const l = typeof locale === "function" ? locale() : locale || "en-US"; + return new Intl.NumberFormat(l, { maximumFractionDigits: 0 }).format(s); }; export const sat = (s: number): string => `${si(Math.abs(s)).toString()}`; export const si = ( - n: number, - minimumFractionDigits = 1, - maximumFractionDigits = 2, + n: number, + minimumFractionDigits = 1, + maximumFractionDigits = 2, ) => { - const units = ["", "K", "M", "G", "T", "P", "E", "Z"]; - - let s = n; - let d = 0; - if (s >= 1000) { - d = Math.floor(Math.log10(s) / 3); // Calculate the unit index - s = n / 1000 ** d; // Scale the number to the correct unit - } - - return ( - new Intl.NumberFormat("en-US", { - minimumFractionDigits, - maximumFractionDigits, - }).format(s) + units[d] - ); + const units = ["", "K", "M", "G", "T", "P", "E", "Z"]; + + let s = n; + let d = 0; + if (s >= 1000) { + d = Math.floor(Math.log10(s) / 3); // Calculate the unit index + s = n / 1000 ** d; // Scale the number to the correct unit + } + + return ( + new Intl.NumberFormat("en-US", { + minimumFractionDigits, + maximumFractionDigits, + }).format(s) + units[d] + ); }; export const sats = 100000000; export const back = (): any => - browser && (history.length ? history.go(-1) : goto("/")); + browser && (history.length ? history.go(-1) : goto("/")); export const focus = (el: HTMLElement): any => - browser && screen.width > 1280 && setTimeout(() => el.focus(), 1); + browser && screen.width > 1280 && setTimeout(() => el.focus(), 1); export const select = (el: HTMLInputElement): any => - browser && screen.width > 1280 && setTimeout(() => el.select(), 1); + browser && screen.width > 1280 && setTimeout(() => el.select(), 1); export const sleep = (n: number): Promise => - new Promise((r) => setTimeout(r, n)); + new Promise((r) => setTimeout(r, n)); export const wait = async ( - f: () => boolean | Promise, - n = 100, - s = 300, + f: () => boolean | Promise, + n = 100, + s = 300, ): Promise => { - let i = 0; - while (!(await f()) && i < s) { - await sleep(n); - i++; - } - if (i >= s) throw new Error("timeout"); - return f(); + let i = 0; + while (!(await f()) && i < s) { + await sleep(n); + i++; + } + if (i >= s) throw new Error("timeout"); + return f(); }; export const types = { ark: "ark", - bitcoin: "bitcoin", - liquid: "liquid", - lightning: "lightning", - internal: "internal", - fund: "fund", - ecash: "ecash", - bolt12: "bolt12", + bitcoin: "bitcoin", + liquid: "liquid", + lightning: "lightning", + internal: "internal", + fund: "fund", + ecash: "ecash", + bolt12: "bolt12", }; export const ease = (t: number): number => - t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; + t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; export function shuffleArray(array: T[]): void { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } } export const closest = (a: number[], n: number): number => - a.reduce((prev, curr) => - Math.abs(curr - n) < Math.abs(prev - n) ? curr : prev, - ); + a.reduce((prev, curr) => + Math.abs(curr - n) < Math.abs(prev - n) ? curr : prev, + ); export const isLiquid = (text: string): boolean => - text.startsWith("Az") || - text.startsWith("lq1") || - text.startsWith("VJL") || - text.startsWith("VT") || - text.startsWith("XR") || - text.startsWith("XC") || - ((text.startsWith("H") || text.startsWith("G") || text.startsWith("Q")) && - text.length === 34) || - (text.startsWith("ert1q") && text.length === 43) || - (text.startsWith("ex1q") && text.length === 42) || - text.startsWith("el1qq") || - text.startsWith("lq1qq"); + text.startsWith("Az") || + text.startsWith("lq1") || + text.startsWith("VJL") || + text.startsWith("VT") || + text.startsWith("XR") || + text.startsWith("XC") || + ((text.startsWith("H") || text.startsWith("G") || text.startsWith("Q")) && + text.length === 34) || + (text.startsWith("ert1q") && text.length === 43) || + (text.startsWith("ex1q") && text.length === 42) || + text.startsWith("el1qq") || + text.startsWith("lq1qq"); export function getCookie(name: string): string | null { - const nameEQ = `${name}=`; - const ca = document.cookie.split(";"); - for (let i = 0; i < ca.length; i++) { - let c = ca[i]; - while (c.charAt(0) === " ") c = c.substring(1); - if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); - } - return null; + const nameEQ = `${name}=`; + const ca = document.cookie.split(";"); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === " ") c = c.substring(1); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); + } + return null; } export function setCookie(name: string, value: string, seconds: number): void { - const now = new Date(); - now.setTime(now.getTime() + seconds * 1000); - const expires = `expires=${now.toUTCString()}`; - document.cookie = `${name}=${value};${expires};path=/`; + const now = new Date(); + now.setTime(now.getTime() + seconds * 1000); + const expires = `expires=${now.toUTCString()}`; + document.cookie = `${name}=${value};${expires};path=/`; } export const network = { - bitcoin: { - bech32: "bc", - pubKeyHash: 0x00, - scriptHash: 0x05, - wif: 0x80, - }, - regtest: { - bech32: "bcrt", - pubKeyHash: 0x6f, - scriptHash: 0xc4, - wif: 0xef, - }, + bitcoin: { + bech32: "bc", + pubKeyHash: 0x00, + scriptHash: 0x05, + wif: 0x80, + }, + regtest: { + bech32: "bcrt", + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef, + }, }[PUBLIC_COINOS_NETWORK]; export const versions = { - bitcoin: { - private: 0x0488ade4, - public: 0x0488b21e, - }, - regtest: { - private: 0x04358394, - public: 0x043587cf, - }, + bitcoin: { + private: 0x0488ade4, + public: 0x0488b21e, + }, + regtest: { + private: 0x04358394, + public: 0x043587cf, + }, }[PUBLIC_COINOS_NETWORK]; export const str = (s) => s.toString(); export const ago = (t) => { - const now = new Date(); - const date = new Date(t * 1000); - const seconds = differenceInSeconds(now, date); - if (seconds < 60) return `${seconds}s`; + const now = new Date(); + const date = new Date(t * 1000); + const seconds = differenceInSeconds(now, date); + if (seconds < 60) return `${seconds}s`; - const minutes = differenceInMinutes(now, date); - if (minutes < 60) return `${minutes}m`; + const minutes = differenceInMinutes(now, date); + if (minutes < 60) return `${minutes}m`; - const hours = differenceInHours(now, date); - if (hours < 24) return `${hours}h`; + const hours = differenceInHours(now, date); + if (hours < 24) return `${hours}h`; - const days = differenceInDays(now, date); - return `${days}d`; + const days = differenceInDays(now, date); + return `${days}d`; }; -export const register = async (user, ip, cookies, loginRedirect, host?, extraHeaders?: Record) => { - let error; - - const headers: Record = { - ...extraHeaders, - }; - if (ip) headers["cf-connecting-ip"] = ip; - - let sk; - try { - ({ sk } = await post("/register", { user }, headers)); - } catch (e) { - ({ message: error } = e as Error); - } - - try { - await login(user, cookies, ip, host, extraHeaders); - error = null; - } catch (e) { - const { message } = e as Error; - error ||= message; - } - - const expires = new Date(); - expires.setSeconds(expires.getSeconds() + 21000000); - cookies.set("sk", sk, { - path: "/", - expires, - httpOnly: false, - sameSite: "lax", - }); - - if (error) return svelteFail(400, { error }); - redirect(303, loginRedirect || `/${user.username}`); +export const register = async ( + user, + ip, + cookies, + loginRedirect, + host?, + extraHeaders?: Record, +) => { + let error; + + const headers: Record = { + ...extraHeaders, + }; + if (ip) headers["cf-connecting-ip"] = ip; + + let sk; + try { + ({ sk } = await post("/register", { user }, headers)); + } catch (e) { + ({ message: error } = e as Error); + } + + try { + await login(user, cookies, ip, host, extraHeaders); + error = null; + } catch (e) { + const { message } = e as Error; + error ||= message; + } + + const expires = new Date(); + expires.setSeconds(expires.getSeconds() + 21000000); + cookies.set("sk", sk, { + path: "/", + expires, + httpOnly: false, + sameSite: "lax", + }); + + if (error) return svelteFail(400, { error }); + redirect(303, loginRedirect || `/${user.username}`); }; diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts index 8b694241..3b29df70 100644 --- a/src/routes/(app)/+layout.server.ts +++ b/src/routes/(app)/+layout.server.ts @@ -2,63 +2,63 @@ import { auth, get, isInvalidTokenError, sleep } from "$lib/utils"; import { error, redirect } from "@sveltejs/kit"; export async function load({ cookies, request, url, params }) { - const { pathname } = url; - const { id, username } = params; - const token = cookies.get("token"); + const { pathname } = url; + const { id, username } = params; + const token = cookies.get("token"); - let user; - if (token) { - try { - user = await get("/me", auth(cookies)); - } catch (e) { - const { message } = e as Error; - if (message.startsWith("Rate")) { - await sleep(3000); - try { - user = await get("/me", auth(cookies)); - } catch (retryError) { - if (isInvalidTokenError(retryError) && pathname !== "/logout") { - redirect(307, "/logout"); - } - throw retryError; - } - } else if (isInvalidTokenError(e) && pathname !== "/logout") { - redirect(307, "/logout"); - } else { - throw e; - } - } - } + let user; + if (token) { + try { + user = await get("/me", auth(cookies)); + } catch (e) { + const { message } = e as Error; + if (message.startsWith("Rate")) { + await sleep(3000); + try { + user = await get("/me", auth(cookies)); + } catch (retryError) { + if (isInvalidTokenError(retryError) && pathname !== "/logout") { + redirect(307, "/logout"); + } + throw retryError; + } + } else if (isInvalidTokenError(e) && pathname !== "/logout") { + redirect(307, "/logout"); + } else { + throw e; + } + } + } - let subject; + let subject; - if (url.pathname.includes("/invoice") && id) { - try { - ({ user: subject } = await get(`/invoice/${id}`)); - } catch (e) { - console.log("unable to fetch invoice", id, e); - } - } else if (username) { - try { - subject = await get(`/users/${username}`); - } catch (e) { - error(500, "Unable to retrieve user account data"); - } - } + if (url.pathname.includes("/invoice") && id) { + try { + ({ user: subject } = await get(`/invoice/${id}`)); + } catch (e) { + console.log("unable to fetch invoice", id, e); + } + } else if (username) { + try { + subject = await get(`/users/${username}`); + } catch { + error(500, "Unable to retrieve user account data"); + } + } - if (user?.needsMigration) { - redirect(307, "/migrate"); - } + if (user?.needsMigration) { + redirect(307, "/migrate"); + } - if ( - user && - ["/login", "/register"].includes(pathname) && - request.method === "GET" - ) { - redirect(307, `/${user.username}`); - } + if ( + user && + ["/login", "/register"].includes(pathname) && + request.method === "GET" + ) { + redirect(307, `/${user.username}`); + } - const theme = cookies.get("theme") || "light"; + const theme = cookies.get("theme") || "light"; - return { user, token, subject, theme }; + return { user, token, subject, theme }; } diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 6ac9bed5..346cc93e 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -24,7 +24,12 @@ import Password from "$comp/Password.svelte"; import { s, f, toFiat, success, getCookie, warning } from "$lib/utils"; import { t, locale, loading } from "$lib/translations"; - import { goto, invalidate, afterNavigate, preloadData } from "$app/navigation"; + import { + goto, + invalidate, + afterNavigate, + preloadData, + } from "$app/navigation"; let { data, children } = $props(); @@ -58,7 +63,9 @@ if (result?.received > 0) { if ($fiat && $rateStore && user?.currency) { - success(`Received ${f(toFiat(result.received, $rateStore), user.currency)}!`); + success( + `Received ${f(toFiat(result.received, $rateStore), user.currency)}!`, + ); } else { success(`Received ⚡️${s(result.received)}!`); } diff --git a/src/routes/(app)/[username]/(profile)/+layout.server.ts b/src/routes/(app)/[username]/(profile)/+layout.server.ts index 251388e5..9c916d7a 100644 --- a/src/routes/(app)/[username]/(profile)/+layout.server.ts +++ b/src/routes/(app)/[username]/(profile)/+layout.server.ts @@ -1,10 +1,10 @@ import getRates from "$lib/rates"; export async function load({ depends, parent }) { - depends("app:user"); + depends("app:user"); - const rates = await getRates(); - const { subject, user } = await parent(); + const rates = await getRates(); + const { subject } = await parent(); - return { rate: rates[subject.currency] }; + return { rate: rates[subject.currency] }; } diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte index 8331c98e..1bb9447f 100644 --- a/src/routes/(app)/[username]/(profile)/+layout.svelte +++ b/src/routes/(app)/[username]/(profile)/+layout.svelte @@ -291,7 +291,7 @@
{#if satsPerCurrency >= 1} -
{f(1, currency, locale)} =
+
{f(1, currency, locale)} =
{ - depends("app:user"); + depends("app:user"); - try { - const subject = await get(`/users/${username}`); - const { user } = await parent(); + try { + const subject = await get(`/users/${username}`); + const { user } = await parent(); - const expires = new Date(); - expires.setSeconds(expires.getSeconds() + 380 * 24 * 60 * 60); - cookies.set("username", username, { path: "/", expires, httpOnly: false }); + const expires = new Date(); + expires.setSeconds(expires.getSeconds() + 380 * 24 * 60 * 60); + cookies.set("username", username, { path: "/", expires, httpOnly: false }); - return { subject, user }; - } catch (e) { - error(500, "Unable to retrieve user account data"); - } + return { subject, user }; + } catch { + error(500, "Unable to retrieve user account data"); + } }; diff --git a/src/routes/(app)/[username]/accepted/[text]/+page.server.ts b/src/routes/(app)/[username]/accepted/[text]/+page.server.ts index aa25ce7f..4da700e0 100644 --- a/src/routes/(app)/[username]/accepted/[text]/+page.server.ts +++ b/src/routes/(app)/[username]/accepted/[text]/+page.server.ts @@ -1,6 +1,6 @@ export async function load({ params, url }) { - const { text } = params; - const query = new URLSearchParams(url.search); - const nfc = !!query.get("nfc"); - return { nfc, text }; + const { text } = params; + const query = new URLSearchParams(url.search); + const nfc = !!query.get("nfc"); + return { nfc, text }; } diff --git a/src/routes/(app)/[username]/accepted/[text]/+page.svelte b/src/routes/(app)/[username]/accepted/[text]/+page.svelte index fd2c7991..339f6c9c 100644 --- a/src/routes/(app)/[username]/accepted/[text]/+page.svelte +++ b/src/routes/(app)/[username]/accepted/[text]/+page.svelte @@ -1,4 +1,5 @@ -
diff --git a/src/routes/(app)/account/ark/+page.svelte b/src/routes/(app)/account/ark/+page.svelte index 0c4d8783..42329ef8 100644 --- a/src/routes/(app)/account/ark/+page.svelte +++ b/src/routes/(app)/account/ark/+page.svelte @@ -9,10 +9,7 @@ import { goto } from "$app/navigation"; import { arkServerUrl } from "$lib/ark"; import { HDKey } from "@scure/bip32"; - import { - entropyToMnemonic, - mnemonicToSeed, - } from "@scure/bip39"; + import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english.js"; import { rememberForOptions, @@ -168,7 +165,8 @@ {#if hasMasterSeed}

- Your Ark wallet will be derived from your master seed. Enter your wallet password to continue. + Your Ark wallet will be derived from your master seed. Enter your + wallet password to continue.