diff options
| author | Adam Soltys <[email protected]> | 2025-01-03 15:24:31 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2025-01-03 15:24:31 -0800 |
| commit | 50296ccde06dc7892b0c6bd1546030f495564b28 (patch) | |
| tree | 444e26008235c9a57ec2756d8b8fa60133d4e4a9 | |
| parent | 1bfdc18184fd57b741f5ebdfb0e323747f4d06b0 (diff) | |
| download | coinos-ui-50296ccde06dc7892b0c6bd1546030f495564b28.tar.gz coinos-ui-50296ccde06dc7892b0c6bd1546030f495564b28.zip | |
nostr sign in
| -rwxr-xr-x | bun.lockb | bin | 187602 -> 192133 bytes | |||
| -rw-r--r-- | package.json | 3 | ||||
| -rw-r--r-- | src/components/Nostr.svelte | 336 | ||||
| -rw-r--r-- | src/locales/en.json | 3 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/(profile)/+page.svelte | 15 | ||||
| -rw-r--r-- | src/routes/(app)/events/+server.ts | 8 | ||||
| -rw-r--r-- | src/routes/(app)/invoice/[id]/paid/+page.svelte | 11 | ||||
| -rw-r--r-- | src/routes/(app)/ln/[lnurl]/+page.server.ts | 2 | ||||
| -rw-r--r-- | src/routes/(app)/pay/[username]/[...amount]/+page.server.ts | 6 | ||||
| -rw-r--r-- | src/routes/(app)/send/invoice/[id]/+page.server.ts | 4 | ||||
| -rw-r--r-- | src/routes/(static)/login/+page.server.ts | 2 | ||||
| -rw-r--r-- | src/routes/(static)/login/+page.svelte | 62 | ||||
| -rw-r--r-- | src/routes/amber/[id]/[pubkey]/+server.ts | 6 |
13 files changed, 391 insertions, 67 deletions
| Binary files differ diff --git a/package.json b/package.json index 07942ef6..a22d3954 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@sveltejs/vite-plugin-svelte": "^4.0.0", "@types/uuid": "^10.0.0", "@zerodevx/svelte-toast": "^0.9.5", + "applesauce-signer": "^0.10.0", "autoprefixer": "^10.4.13", "bech32": "^2.0.0", "bip21": "^2.0.3", @@ -30,7 +31,7 @@ "leaflet": "^1.9.3", "leaflet.markercluster": "^1.5.3", "maplibre-gl": "^3.3.1", - "nostr-tools": "^2.10.3", + "nostr-tools": "^2.10.4", "postcss": "^8.4.20", "prettier": "^3.1.0", "prettier-plugin-svelte": "^3.2.6", diff --git a/src/components/Nostr.svelte b/src/components/Nostr.svelte new file mode 100644 index 00000000..6445c4c1 --- /dev/null +++ b/src/components/Nostr.svelte @@ -0,0 +1,336 @@ +<script lang="ts"> + import { applyAction, deserialize } from "$app/forms"; + import { onMount } from "svelte"; + import { browser } from "$app/environment"; + import { t } from "$lib/translations"; + import PasswordInput from "$comp/PasswordInput.svelte"; + import { Relay } from "nostr-tools/relay"; + import { encrypt, decrypt, getConversationKey } from "nostr-tools/nip44"; + import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; + import { + nip19, + getEventHash, + finalizeEvent, + generateSecretKey, + getPublicKey, + } from "nostr-tools"; + import { AmberClipboardSigner } from "applesauce-signer"; + + import { focus, fail, post } from "$lib/utils"; + import { password as pw, passwordPrompt } from "$lib/store"; + + let { id, redirect, token, nostrSignin } = $props(); + let extension = $state(); + + onMount(() => { + if (browser && window.nostr) extension = true; + }); + + let cancel = () => (nostrSignin = false); + + let sk = $state(); + let ck = $state(); + let pk = $state(); + let relay = $state(); + + let signer = $derived( + browser && + navigator.userAgent.includes("Android") && + navigator.clipboard && + navigator.clipboard.readText, + ); + + let connectUrl = $state(); + // let bunkerConnect = async () => { + // sk = generateSecretKey(); + // console.log("SK", bytesToHex(sk)); + // let pubkey = getPublicKey(sk); + // console.log("PUBKEY", pubkey); + // + // const url = new URL(bunker); + // const secret = url.searchParams.get("secret"); + // console.log("URL", url); + // pk = url.host || url.pathname.replace("//", ""); + // console.log("PK", pk); + // const relayUrl = url.searchParams.get("relay"); + // relay = await Relay.connect(relayUrl); + // console.log("connecting to", relayUrl); + // + // ck = getConversationKey(sk, pk); + // + // console.log("PK", pk); + // console.log("CK", bytesToHex(ck)); + // + // let connect = { + // id: crypto.randomUUID(), + // method: "connect", + // params: [pk, secret, "get_public_key,sign_event"], + // }; + // + // let event = { + // created_at: Math.round(Date.now() / 1000), + // kind: 24133, + // pubkey, + // content: encrypt(JSON.stringify(connect), ck), + // tags: [["p", pk]], + // }; + // + // let signedEvent = finalizeEvent(event, sk); + // console.log("publishing connection event", signedEvent); + // await relay.publish(signedEvent); + // + // const sub = relay.subscribe([{ kinds: [24133], "#p": [pubkey] }], { + // async onevent(ev) { + // console.log("got event:", ev); + // try { + // console.log(decrypt(ev.content, ck)); + // } catch (e) { + // console.log(e); + // } + // }, + // }); + // }; + + let getPubkey = async () => { + let pubkey = getPublicKey(sk); + let getPubkey = { + id: crypto.randomUUID(), + method: "get_public_key", + params: [], + }; + + let testEvent = { + created_at: Math.round(Date.now() / 1000), + kind: 1, + content: "hi mom", + tags: [], + }; + + let signEvent = { + id: crypto.randomUUID(), + method: "sign_event", + params: [JSON.stringify(testEvent)], + }; + + let event = { + created_at: Math.round(Date.now() / 1000), + kind: 24133, + pubkey, + content: encrypt(JSON.stringify(signEvent), ck), + tags: [["p", pk]], + }; + + let signedEvent = finalizeEvent(event, sk); + await relay.publish(signedEvent); + }; + + let nostrConnect = async () => { + let secret = crypto.randomUUID(); + let sk = generateSecretKey(); + console.log("SK", bytesToHex(sk)); + let pubkey = getPublicKey(sk); + console.log("PUBKEY", pubkey); + connectUrl = `nostrconnect://${pubkey}?relay=wss%3A%2F%2Frelay.coinos.io&perms=nip44_encrypt%2Cnip44_decrypt%2Csign_event%3A13%2Csign_event%3A14%2Csign_event%3A1059&name=Coinos&secret=${secret}`; + + const relay = await Relay.connect("wss://relay.coinos.io"); + console.log(`connected to ${relay.url}`); + let finished, remoteSignerPubkey; + const sub = relay.subscribe([{ kinds: [24133], "#p": [pubkey] }], { + async onevent(event) { + console.log("got event:", event); + remoteSignerPubkey = event.pubkey; + // console.log("decrypted", decrypt(event.content, sk)); + if (!finished) { + finished = true; + let event = { + created_at: Math.round(Date.now() / 1000), + kind: 24133, + pubkey, + content: encrypt( + JSON.stringify({ + id: "1234", + method: "get_public_key", + params: [], + }), + sk, + hexToBytes(remoteSignerPubkey), + ), + tags: [["p", remoteSignerPubkey]], + }; + + console.log("asking for public key", event); + console.log("DECRYPTED", decrypt(event.content, sk)); + let signedEvent = finalizeEvent(event, sk); + console.log("sleeping"); + await new Promise((r) => setTimeout(r, 5000)); + console.log("sending"); + await relay.publish(signedEvent); + } + }, + }); + console.log("subbed"); + }; + + let pubkey = $state(); + let ev = $derived({ + kind: 1, + created_at: Date.now(), + content: id, + tags: [], + }); + + let nsec = $state(); + let showNsec = $state(); + let toggleNsec = () => (showNsec = !showNsec); + let nsecSign = async () => { + let sk; + if (nsec.startsWith("nsec")) sk = nip19.decode(nsec).data as Uint8Array; + else sk = hexToBytes(nsec); + + nostrLogin(finalizeEvent(ev, sk)); + }; + + let extensionSign = async () => { + nostrLogin(await window.nostr.signEvent(ev)); + }; + + let signerGetPubkey = async () => { + const signer = new AmberClipboardSigner(); + pubkey = await signer.getPublicKey(); + ev.pubkey = pubkey; + console.log("PUBKEY", ev.pubkey); + }; + // + // let signerSign = async () => { + // const signer = new AmberClipboardSigner(); + // ev.sig = await signer.signEvent(ev); + // nostrLogin(await signer.signEvent(ev)); + // }; + // + let signerSign = async () => { + document.addEventListener("focus", signerSigned, true); + }; + + const signerSigned = async () => { + document.removeEventListener("focus", signerSigned, true); + await new Promise((resolve) => setTimeout(resolve, 500)); + ev.sig = await navigator.clipboard.readText(); + ev.id = getEventHash(ev); + nostrLogin(ev); + }; + + let nostrLogin = async (signedEvent) => { + // let pubkey = await window.nostr.getPublicKey(); + const formData = new FormData(); + + console.log("SIGNED", signedEvent); + + try { + formData.append("loginRedirect", redirect); + formData.append("token", token); + formData.append("event", JSON.stringify(signedEvent)); + formData.append("id", id); + + let response = await fetch("/login?/nostr", { + method: "POST", + body: formData, + }); + + const result = deserialize(await response.text()); + + if (result.type === "success") { + await invalidateAll(); + } + + applyAction(result); + } catch (e) { + fail(e.message); + } + }; +</script> + +{#if nostrSignin} + <div + class="fixed inset-0 bg-base-100 bg-opacity-90 overflow-y-auto h-full w-full z-50" + > + <div + class="relative mx-auto mt-10 p-5 border w-96 shadow-lg rounded-md bg-base-100 space-y-5" + > + <h1 class="text-center text-2xl font-semibold"> + {$t("login.nostr")} + </h1> + + <!-- <div class="break-all"> --> + <!-- {JSON.stringify(ev)} --> + <!-- </div> --> + {#if pubkey} + <a + href={`nostrsigner:${encodeURIComponent(JSON.stringify(ev))}?compressionType=none&returnType=signature&type=sign_event&appName=Coinos`} + type="button" + class="btn" + onclick={signerSign} + > + <iconify-icon + icon="material-symbols:diamond-rounded" + width="32" + class="text-orange-400" + ></iconify-icon> + <div class="my-auto">NIP-55 Signer Step 2</div> + </a> + {:else} + {#if extension} + <button type="button" class="btn" onclick={extensionSign}> + <iconify-icon + icon="lucide-lab:bee" + width="32" + class="text-yellow-400" + ></iconify-icon> + <div class="my-auto">NIP-10 Extension</div> + </button> + {/if} + <!-- {#if connectUrl} --> + <!-- <a href={connectUrl}> --> + <!-- connect --> + <!-- <img --> + <!-- src={`/qr/${encodeURIComponent(connectUrl)}/raw`} --> + <!-- class="z-10 border-4 border-white" --> + <!-- alt="QR" --> + <!-- /> --> + <!-- <div class="break-all"> --> + <!-- {connectUrl} --> + <!-- </div> --> + <!-- </a> --> + <!-- <button class="btn" onclick={() => copy(connectUrl)}>Copy</button> --> + <!-- {:else} --> + <!-- <button type="button" class="btn" onclick={nostrConnect}> --> + <!-- <img src="/images/nostr.png" class="w-8" /> --> + <!-- <div class="my-auto">NIP-46 Connect</div> --> + <!-- </button> --> + <!-- {/if} --> + {#if signer} + <a type="button" class="btn" onclick={signerGetPubkey}> + <iconify-icon + icon="material-symbols:diamond-rounded" + width="32" + class="text-orange-400" + ></iconify-icon> + <div class="my-auto">NIP-55 Signer</div> + </a> + {/if} + + {#if showNsec} + <input bind:value={nsec} placeholder="nsec..." /> + <button type="button" class="btn" onclick={nsecSign}> + <iconify-icon icon="ph:key-bold" width="32"></iconify-icon> + <div class="my-auto">Nsec login</div> + </button> + {:else} + <button type="button" class="btn" onclick={toggleNsec}> + <iconify-icon icon="ph:key-bold" width="32"></iconify-icon> + <div class="my-auto">Nsec login</div> + </button> + {/if} + {/if} + </div> + </div> +{/if} diff --git a/src/locales/en.json b/src/locales/en.json index 937b3be3..848b467d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -168,7 +168,8 @@ "signIn": "Sign In", "signUp": "Sign Up", "failed": "Login failed", - "nostr": "Sign in with Nostr" + "nostr": "Sign in with Nostr", + "nostrConnect": "Nostr connect" }, "nav": { "dark": "Dark", diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte index a498f0f7..6530f149 100644 --- a/src/routes/(app)/[username]/(profile)/+page.svelte +++ b/src/routes/(app)/[username]/(profile)/+page.svelte @@ -8,6 +8,7 @@ import { t } from "$lib/translations"; import { installPrompt } from "$lib/store"; import { afterNavigate, preloadData } from "$app/navigation"; + import { page } from "$app/stores"; let { data } = $props(); @@ -24,7 +25,9 @@ $installPrompt = null; }; + let pubkey = $state(); if (user) user.savings = 0; + let { host } = $derived($page.url); </script> <div class="space-y-2"> @@ -37,14 +40,16 @@ <a href={`/account/savings`} class="contents"> <button class="btn btn-lg w-full rounded-2xl whitespace-nowrap"> - <iconify-icon noobserver icon="ph:plus-circle-bold" width="32"></iconify-icon> + <iconify-icon noobserver icon="ph:plus-circle-bold" width="32" + ></iconify-icon> {$t("accounts.addAccount")} </button> </a> {#if $installPrompt} <button class="btn btn-accent lg:hidden" onclick={install}> - <iconify-icon noobserver icon="ph:floppy-disk-bold" width="32"></iconify-icon> + <iconify-icon noobserver icon="ph:floppy-disk-bold" width="32" + ></iconify-icon> {$t("user.install")} </button> {/if} @@ -62,7 +67,10 @@ <button class="btn btn-accent !text-2xl items-center !w-full sm:!max-w-[400px] mx-auto" > - <iconify-icon noobserver icon="ph:lightning-fill" class="text-yellow-300" + <iconify-icon + noobserver + icon="ph:lightning-fill" + class="text-yellow-300" ></iconify-icon> <div> {$t("user.pay")} @@ -72,3 +80,4 @@ </a> {/if} </div> + diff --git a/src/routes/(app)/events/+server.ts b/src/routes/(app)/events/+server.ts index 42b9f3dd..59fc472e 100644 --- a/src/routes/(app)/events/+server.ts +++ b/src/routes/(app)/events/+server.ts @@ -1,5 +1,11 @@ import { auth, post } from "$lib/utils"; -import { json, error } from "@sveltejs/kit"; +import { error, json } from "@sveltejs/kit"; + +export async function GET({ url }) { + console.log(url.searchParams.get("event")); + console.log(url.searchParams.get("uid")); + return json({}); +} export async function POST({ cookies, request }) { const body = await request.json(); diff --git a/src/routes/(app)/invoice/[id]/paid/+page.svelte b/src/routes/(app)/invoice/[id]/paid/+page.svelte index a13692d7..bc61d7fe 100644 --- a/src/routes/(app)/invoice/[id]/paid/+page.svelte +++ b/src/routes/(app)/invoice/[id]/paid/+page.svelte @@ -2,29 +2,36 @@ import Amount from "$comp/Amount.svelte"; import Success from "$comp/Success.svelte"; import { t } from "$lib/translations"; + import { loc } from "$lib/utils"; let { data } = $props(); let { amount, currency, rate, received, pending, tip, user, id } = $derived( data.invoice, ); + let locale = $derived(loc(user)); // toast.pop(0); </script> <div class="container px-4 text-center mx-auto"> {#if pending} - <iconify-icon noobserver icon="ph:clock-bold" class="text-warning" width="160" + <iconify-icon + noobserver + icon="ph:clock-bold" + class="text-warning" + width="160" ></iconify-icon> <h1 class="text-3xl md:text-4xl font-bold mb-6">Payment detected</h1> - <Amount amount={pending - tip} {tip} {rate} {currency} /> + <Amount amount={pending - tip} {tip} {rate} {currency} {locale} /> {:else} <Success amount={received - tip} {rate} {tip} {currency} + {locale} title={$t("invoice.paymentSuccessful")} /> {/if} diff --git a/src/routes/(app)/ln/[lnurl]/+page.server.ts b/src/routes/(app)/ln/[lnurl]/+page.server.ts index 53445461..7b63e6f5 100644 --- a/src/routes/(app)/ln/[lnurl]/+page.server.ts +++ b/src/routes/(app)/ln/[lnurl]/+page.server.ts @@ -7,12 +7,10 @@ export async function load({ params, parent }) { if (!user) redirect(307, "/register"); const rates = await getRates(); - console.log("YO"); let data; try { const { lnurl } = params; data = await get(`/decode?text=${lnurl}`); - console.log("DATA", data); } catch (e) { const { message } = e as Error; error(500, message); diff --git a/src/routes/(app)/pay/[username]/[...amount]/+page.server.ts b/src/routes/(app)/pay/[username]/[...amount]/+page.server.ts index 812b97ec..b0f824ed 100644 --- a/src/routes/(app)/pay/[username]/[...amount]/+page.server.ts +++ b/src/routes/(app)/pay/[username]/[...amount]/+page.server.ts @@ -45,11 +45,8 @@ export const actions = { default: async ({ cookies, request }) => { let id; const body = await fd(request); - console.log("BODY", body); const { amount, rate, prompt, type, username } = body; - console.log("AMT", amount); - try { ({ id } = await post( "/invoice", @@ -69,7 +66,8 @@ export const actions = { return fail(400, { message }); } - if (prompt) redirect(307, `/invoice/${id}/tip`); + if (parseInt(amount) > 0 && prompt) redirect(307, `/invoice/${id}/tip`); + else if (parseInt(amount) <= 0 ) redirect(307, `/${username}/receive`); redirect(307, `/invoice/${id}`); }, }; diff --git a/src/routes/(app)/send/invoice/[id]/+page.server.ts b/src/routes/(app)/send/invoice/[id]/+page.server.ts index 9b971f2f..658629ef 100644 --- a/src/routes/(app)/send/invoice/[id]/+page.server.ts +++ b/src/routes/(app)/send/invoice/[id]/+page.server.ts @@ -8,7 +8,7 @@ export async function load({ cookies, params: { id }, parent }) { const invoice = await get(`/invoice/${id}`); - if (invoice.prompt && invoice.tip === null) + if (invoice.amount && invoice.prompt && invoice.tip === null) redirect(307, `/invoice/${id}/tip`); if (invoice.memoPrompt && invoice.memo === null) @@ -17,7 +17,7 @@ export async function load({ cookies, params: { id }, parent }) { if (user && invoice.aid === aid) error(500, { message: "Cannot send to self" }); else if (user && ![types.lightning, types.bolt12].includes(invoice.type)) - redirect(307, `/send/${invoice.type}/${invoice.hash}`); + redirect(307, `/send/${invoice.type === types.ecash ? "ecash" : "bitcoin"}/${invoice.hash}`); if (!user) redirect(307, `/invoice/${id}`); diff --git a/src/routes/(static)/login/+page.server.ts b/src/routes/(static)/login/+page.server.ts index 975dab5d..6bf06ad2 100644 --- a/src/routes/(static)/login/+page.server.ts +++ b/src/routes/(static)/login/+page.server.ts @@ -1,6 +1,6 @@ +import { PUBLIC_COINOS_URL } from "$env/static/public"; import { fd, get, login } from "$lib/utils"; import { fail, redirect } from "@sveltejs/kit"; -import { PUBLIC_COINOS_URL } from "$env/static/public"; export const load = async ({ parent }) => { const { user } = await parent(); diff --git a/src/routes/(static)/login/+page.svelte b/src/routes/(static)/login/+page.svelte index bfd30fe2..dec02b17 100644 --- a/src/routes/(static)/login/+page.svelte +++ b/src/routes/(static)/login/+page.svelte @@ -3,21 +3,22 @@ import handler from "$lib/handler"; import { onMount } from "svelte"; - import { applyAction, deserialize } from "$app/forms"; + import { deserialize } from "$app/forms"; import { tick } from "svelte"; import { fly } from "svelte/transition"; import { enhance } from "$app/forms"; + import Nostr from "$comp/Nostr.svelte"; import Pinpad from "$comp/Pinpad.svelte"; import PasswordInput from "$comp/PasswordInput.svelte"; - import { focus, fail } from "$lib/utils"; + import { wait, copy, focus, fail } from "$lib/utils"; import { password, pin, loginRedirect } from "$lib/store"; import { t } from "$lib/translations"; import { page } from "$app/stores"; import { invalidateAll } from "$app/navigation"; - let nostr = $state(); + let { data, form } = $props(); + onMount(() => { - if (browser && window.nostr) nostr = true; $pin = undefined; localStorage.clear(); sessionStorage.clear(); @@ -26,11 +27,10 @@ if (lang) document.cookie = `lang=${lang} ;`; }); - let { data, form } = $props(); let { id } = $derived(data); + let nostrSignin = $state(); let token = $state(""); - let code = []; $password = undefined; @@ -78,43 +78,6 @@ }); }); - let nostrLogin = async () => { - let pubkey = await window.nostr.getPublicKey(); - const formData = new FormData(); - - let ev = { - kind: 1, - pubkey, - created_at: Date.now(), - content: id, - tags: [], - }; - - try { - let signedEvent = await window.nostr.signEvent(ev); - - formData.append("loginRedirect", redirect); - formData.append("token", token); - formData.append("event", JSON.stringify(signedEvent)); - formData.append("id", id); - - let response = await fetch("?/nostr", { - method: "POST", - body: formData, - }); - - const result = deserialize(await response.text()); - - if (result.type === "success") { - await invalidateAll(); - } - - applyAction(result); - } catch (e) { - fail(e.message); - } - }; - let redirect = $derived( $loginRedirect || $page.url.searchParams.get("redirect"), ); @@ -163,13 +126,10 @@ {$t("login.signIn")} </button> - {#if nostr} - <button type="button" class="btn" onclick={nostrLogin}> - <img src="/images/nostr.png" class="w-8" /> - <div class="my-auto">{$t("login.nostr")}</div> - </button> - {/if} - + <button type="button" class="btn" onclick={() => (nostrSignin = true)}> + <img src="/images/nostr.png" class="w-8" /> + <div class="my-auto">{$t("login.nostr")}</div> + </button> <p class="text-secondary text-center font-medium"> {$t("login.noAccount")} <a @@ -203,3 +163,5 @@ </div> </div> {/if} + +<Nostr {id} {nostrSignin} {redirect} {token} /> diff --git a/src/routes/amber/[id]/[pubkey]/+server.ts b/src/routes/amber/[id]/[pubkey]/+server.ts new file mode 100644 index 00000000..6a6f69cf --- /dev/null +++ b/src/routes/amber/[id]/[pubkey]/+server.ts @@ -0,0 +1,6 @@ +import { json } from "@sveltejs/kit"; + +export async function GET({ params }) { + console.log(params); + return json({}); +} |
