diff options
| author | Adam Soltys <[email protected]> | 2024-09-12 15:23:45 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2024-09-17 09:19:25 -0700 |
| commit | 2114156be3f89a2ef4c5bd13fd6d7a51d8fbc135 (patch) | |
| tree | ae764021c0f9017493366862d42ea6cac904a76e /src/routes/(app) | |
| parent | 1f63848ca9fb01336f04b41434de1cc06a5b3d93 (diff) | |
| download | coinos-ui-2114156be3f89a2ef4c5bd13fd6d7a51d8fbc135.tar.gz coinos-ui-2114156be3f89a2ef4c5bd13fd6d7a51d8fbc135.zip | |
self custodial send
Diffstat (limited to 'src/routes/(app)')
18 files changed, 257 insertions, 66 deletions
diff --git a/src/routes/(app)/+error.svelte b/src/routes/(app)/+error.svelte new file mode 100644 index 00000000..6ce7979c --- /dev/null +++ b/src/routes/(app)/+error.svelte @@ -0,0 +1,38 @@ +<script> + import { back } from "$lib/utils"; + import Icon from "$comp/Icon.svelte"; + import { page } from "$app/stores"; + + export let data; + let { user } = data; + + let link = user ? `/${user.username}` : "/"; +</script> + +<div class="container px-4 max-w-xl mx-auto mt-10 space-y-5"> + <div class="w-full flex"> + <a href="/" class="mx-auto"> + <Icon icon="logo" /> + </a> + </div> + + <div class="flex justify-center items-center"> + <div class="shadow-xl rounded-3xl px-10 pt-5 pb-10 space-y-5 w-full mx-5"> + <div class="relative"> + <h1 class="text-2xl md:text-3xl font-semibold text-center">Error</h1> + </div> + <p class="text-secondary text-center">Something went wrong</p> + <p class="text-secondary text-center">{$page.error.message}</p> + + <div class="flex justify-center py-24"> + <a href={link}> + <button + class="bg-black text-white border rounded-full px-8 py-4 font-bold hover:opacity-80 text-xl" + > + Back + </button> + </a> + </div> + </div> + </div> +</div> diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 0ff4816d..05ca0d0c 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -17,7 +17,6 @@ import AppHeader from "$comp/AppHeader.svelte"; import Invoice from "$comp/Invoice.svelte"; import Password from "$comp/Password.svelte"; - import Request from "$comp/Request.svelte"; import { getCookie, warning, protectedRoutes } from "$lib/utils"; import { t, locale, loading } from "$lib/translations"; import { goto, onNavigate } from "$app/navigation"; @@ -36,7 +35,6 @@ }); let { generate, rate, user, subject, token, rates } = data; - console.log("SUB", subject) $: update(data); let update = (data) => { @@ -127,10 +125,6 @@ <Invoice {user} /> {/if} -{#if $request} - <Request {user} /> -{/if} - <style> @keyframes fade-in { from { diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte index 9e5babd2..98b77fc9 100644 --- a/src/routes/(app)/[username]/(profile)/+page.svelte +++ b/src/routes/(app)/[username]/(profile)/+page.svelte @@ -40,30 +40,32 @@ <Account {user} {rate} {account} /> {/each} - <button - class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-black text-white" - > - <a href={`/account/setup`}> - <div class="mx-auto flex gap-2"> - <Icon icon="plus" style="w-8 mx-auto invert" /> - <div class="my-auto text-xl whitespace-nowrap">Add account</div> - </div> - </a> - </button> - - {#if $installPrompt} + <div class="flex justify-center gap-2"> <button - class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-black text-white grow basis-full fixed bottom-16 xl:hidden" - on:click={install} + class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-black text-white w-full justify-center" > - <div class="mx-auto flex gap-2"> - <Icon icon="save" style="w-8 mx-auto invert" /> - <div class="my-auto text-xl whitespace-nowrap"> - {$t("user.install")} + <a href={`/account/setup`}> + <div class="mx-auto flex gap-2"> + <Icon icon="plus" style="w-8 mx-auto invert" /> + <div class="my-auto text-xl whitespace-nowrap">Add account</div> </div> - </div> + </a> </button> - {/if} + + {#if $installPrompt} + <button + class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-black text-white xl:hidden w-full justify-center" + on:click={install} + > + <div class="mx-auto flex gap-2"> + <Icon icon="save" style="w-8 mx-auto invert" /> + <div class="my-auto text-xl whitespace-nowrap"> + {$t("user.install")} + </div> + </div> + </button> + {/if} + </div> {/if} <Items {items} {subject} {user} {total} {currency} {rate} /> diff --git a/src/routes/(app)/[username]/+layout.server.ts b/src/routes/(app)/[username]/+layout.server.ts index c340544d..9a520036 100644 --- a/src/routes/(app)/[username]/+layout.server.ts +++ b/src/routes/(app)/[username]/+layout.server.ts @@ -13,7 +13,6 @@ export let load = async ({ let subject = await get(`/users/${username}`); let { user } = await parent(); if (!user) user = subject; - console.log("SUBJECT", subject) let expires = new Date(); expires.setSeconds(expires.getSeconds() + 380 * 24 * 60 * 60); diff --git a/src/routes/(app)/account/setup/step2/+page.svelte b/src/routes/(app)/account/setup/step2/+page.svelte index 8169ce06..221e545e 100644 --- a/src/routes/(app)/account/setup/step2/+page.svelte +++ b/src/routes/(app)/account/setup/step2/+page.svelte @@ -1,28 +1,44 @@ <script lang="ts"> - import { focus } from "$lib/utils"; import { t } from "$lib/translations"; import { mnemonic } from "$lib/store"; import Icon from "$comp/Icon.svelte"; - import { encrypt, decrypt } from "nostr-tools/nip49"; - import { mnemonicToEntropy, entropyToMnemonic } from "@scure/bip39"; + import { encrypt } from "nostr-tools/nip49"; + import { + mnemonicToSeed, + mnemonicToEntropy, + entropyToMnemonic, + } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; - import { fail, post } from "$lib/utils"; + import { focus, versions, fail, post } from "$lib/utils"; import { goto } from "$app/navigation"; + import { HDKey } from "@scure/bip32"; export let data; + let { user } = data; let password; let submit = async () => { let seed = await encrypt(mnemonicToEntropy($mnemonic, wordlist), password); + let master = HDKey.fromMasterSeed( + await mnemonicToSeed($mnemonic), + versions + ); + let child = master.derive("m/84'/0'/0'"); + let pubkey = child.publicExtendedKey; + let fingerprint = child.fingerprint.toString(16).padStart(8, "0"); + + let receiving = child.deriveChild(0); try { - await post("/api/accounts", { seed }); + await post("/api/accounts", { fingerprint, pubkey, seed }); goto(`/${user.username}`); } catch (e) { console.log(e); fail("Something went wrong"); } + + $mnemonic = ""; }; </script> @@ -41,7 +57,7 @@ than your login password. We can't reset it if you lose it. </p> - <input use:focus type="text" placeholder="Password" bind:value={password} /> + <input use:focus type="text" placeholder="Password" bind:value={password} /> <button class="bg-black text-white w-full px-5 py-6 text-xl rounded-2xl font-bold" diff --git a/src/routes/(app)/invoice/+page.server.ts b/src/routes/(app)/invoice/+page.server.ts index 83127457..05851430 100644 --- a/src/routes/(app)/invoice/+page.server.ts +++ b/src/routes/(app)/invoice/+page.server.ts @@ -2,8 +2,8 @@ import { auth, post } from "$lib/utils"; import { error, redirect } from "@sveltejs/kit"; import type { Invoice } from "$lib/types"; -export let load = async ({ cookies, params, parent }) => { - let { account } = params; +export let load = async ({ cookies, parent }) => { + let account = cookies.get("account"); let { subject, user, rates } = await parent(); let invoice: Invoice = { diff --git a/src/routes/(app)/invoice/[id]/+layout.server.ts b/src/routes/(app)/invoice/[id]/+layout.server.ts index e7144b15..71f53e09 100644 --- a/src/routes/(app)/invoice/[id]/+layout.server.ts +++ b/src/routes/(app)/invoice/[id]/+layout.server.ts @@ -1,4 +1,4 @@ -import Qr from "qrcode-base64"; +import * as Qr from "qrcode-base64"; import { get } from "$lib/utils"; import { redirect } from "@sveltejs/kit"; @@ -26,11 +26,10 @@ export async function load({ depends, params, url, parent }) { (!amount && (pending || received)) || (amount > 0 && (pending >= amount || received >= amount)); if (paid && !url.pathname.endsWith("paid")) { - redirect( 307, `/invoice/${id}/paid` + (options ? "?options=true" : "") - ); + redirect(307, `/invoice/${id}/paid` + (options ? "?options=true" : "")); } - let src = Qr.drawImg(invoice.text || "", { size: 340 }); + let src = Qr.drawImg(invoice.text || "", { size: 500 }); return { id, invoice, subject, src }; } diff --git a/src/routes/(app)/invoice/[id]/+page.svelte b/src/routes/(app)/invoice/[id]/+page.svelte index c4810889..36f5d261 100644 --- a/src/routes/(app)/invoice/[id]/+page.svelte +++ b/src/routes/(app)/invoice/[id]/+page.svelte @@ -136,14 +136,14 @@ > <img {src} - class="mx-auto z-10 shadow-inner rounded-2xl" + class="mx-auto z-10 rounded-2xl w-[260px] rounded-2xl" bind:this={qr} alt={txt} /> </a> </div> {:else} - <div class="break-all pb-5 text-lg text-center"> + <div class="break-all pb-5 text-center text-secondary"> {txt} </div> {/if} diff --git a/src/routes/(app)/logout/+page.server.ts b/src/routes/(app)/logout/+page.server.ts index ab5df7b9..9c2a029c 100644 --- a/src/routes/(app)/logout/+page.server.ts +++ b/src/routes/(app)/logout/+page.server.ts @@ -7,6 +7,7 @@ const opts = { export async function load({ cookies }) { let lang = cookies.get("lang"); + cookies.set("account", "", opts); cookies.set("lang", "", opts); cookies.set("username", "", opts); cookies.set("token", "", opts); diff --git a/src/routes/(app)/payments/[...page]/+page.server.ts b/src/routes/(app)/payments/[...page]/+page.server.ts index c2cd5683..e781022e 100644 --- a/src/routes/(app)/payments/[...page]/+page.server.ts +++ b/src/routes/(app)/payments/[...page]/+page.server.ts @@ -5,6 +5,7 @@ let limit = 25; export async function load({ cookies, depends, params }) { depends("app:payments"); + let account = cookies.get("account"); let parts = params.page.split("/"); let start, end, page; @@ -16,7 +17,7 @@ export async function load({ cookies, depends, params }) { let offset = (page - 1) * limit; if (!start) start = Date.now() / 1000 - 24 * 60 * 60; - let url = `/payments?account=${params.account}&start=${ + let url = `/payments?account=${account}&start=${ start * 1000 }&limit=${limit}&offset=${offset}`; if (end) url += `&end=${end * 1000}`; diff --git a/src/routes/(app)/qr/[text]/+page.svelte b/src/routes/(app)/qr/[text]/+page.svelte index c9562582..8b6d48ab 100644 --- a/src/routes/(app)/qr/[text]/+page.svelte +++ b/src/routes/(app)/qr/[text]/+page.svelte @@ -6,10 +6,6 @@ let { src, text } = data; </script> -<button class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80" on:click={back}> - <Icon icon="arrow-left" style="w-10" /> -</button> - <div class="container px-4 max-w-4xl mx-auto space-y-5"> <div class="flex justify-center items-center"> <div diff --git a/src/routes/(app)/send/bitcoin/[address]/+page.server.ts b/src/routes/(app)/send/bitcoin/[address]/+page.server.ts new file mode 100644 index 00000000..3e56e15a --- /dev/null +++ b/src/routes/(app)/send/bitcoin/[address]/+page.server.ts @@ -0,0 +1,7 @@ +import { auth, get } from "$lib/utils"; + +export async function load({ cookies }) { + let account = cookies.get("account"); + let { balance } = await get(`/account/${account}`, auth(cookies)); + return { balance }; +} diff --git a/src/routes/(app)/send/bitcoin/[address]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/+page.svelte index 838c5c04..b656dc06 100644 --- a/src/routes/(app)/send/bitcoin/[address]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/+page.svelte @@ -8,9 +8,9 @@ export let data; - let { rates, user } = data; + let { balance, rates, user } = data; let { address } = $page.params; - let { balance, currency, username } = user; + let { currency, username } = user; let amount = 0; let submit, fiat; diff --git a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts index 7efe6e49..33e96595 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts @@ -1,17 +1,31 @@ import { fail, redirect } from "@sveltejs/kit"; -import { auth, post, fd } from "$lib/utils"; +import { auth, get, post, fd } from "$lib/utils"; export async function load({ params: { address, amount, feeRate }, cookies }) { + let account = cookies.get("account"); + try { - let { fee, fees, ourfee, hex } = await post( + let { fee, fees, inputs, ourfee, hex } = await post( "/bitcoin/fee", - { address, amount, feeRate }, + { address, amount, feeRate, account }, auth(cookies) ); - return { amount, address, fee, fees, feeRate, ourfee, hex }; + account = await get(`/account/${account}`, auth(cookies)); + + return { + account, + amount, + address, + fee, + fees, + feeRate, + ourfee, + hex, + inputs, + }; } catch (e: any) { - return { amount, address, feeRate, message: e.message }; + return { account, amount, address, feeRate, message: e.message }; } } @@ -22,7 +36,8 @@ export const actions = { request, }) => { try { - await post("/bitcoin/send", await fd(request), auth(cookies)); + let body = await fd(request); + await post("/bitcoin/send", body, auth(cookies)); } catch (e: any) { console.log("problem sending bitcoin", e); return fail(400, { address, amount, feeRate, message: e.message }); diff --git a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte index 84506e6b..1267a992 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte @@ -1,20 +1,84 @@ <script> + import { tick } from "svelte"; import { t } from "$lib/translations"; import { enhance } from "$app/forms"; import Toggle from "$comp/Toggle.svelte"; import Icon from "$comp/Icon.svelte"; import Spinner from "$comp/Spinner.svelte"; import { page } from "$app/stores"; - import { fiat as toFiat, f, focus, s, sat, closest } from "$lib/utils"; + import { fiat as toFiat, f, focus, s, sat, closest, network } from "$lib/utils"; import { pin } from "$lib/store"; import { goto, invalidate } from "$app/navigation"; import { rate } from "$lib/store"; + import { applyAction } from "$app/forms"; + import { hex as hexUtil } from "@scure/base"; + import * as btc from "@scure/btc-signer"; + import { decrypt } from "nostr-tools/nip49"; + import { HDKey } from "@scure/bip32"; + import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39"; + import { wordlist } from "@scure/bip39/wordlists/english"; export let data; export let form; - let { amount, address, message, fee, fees, feeRate, ourfee, rates, hex } = - data; + let { encode, decode } = hexUtil; + let revealPassword = false; + let passwordPrompt; + let password; + let cancel = () => (passwordPrompt = false); + let signed; + + let togglePassword = () => (passwordPrompt = !passwordPrompt); + let handler = ({ cancel }) => { + if (account.seed && !signed) cancel(), togglePassword(); + return async ({ result }) => { + if (result.type === "redirect") { + goto(result.location); + } else { + await applyAction(result); + } + }; + }; + + let signTx = async () => { + let entropy = await decrypt(account.seed, password); + let mnemonic = entropyToMnemonic(entropy, wordlist); + let seed = await mnemonicToSeed(mnemonic); + let master = HDKey.fromMasterSeed(seed, network); + let child = master.derive("m/84'/0'/0'"); + + let tx = btc.Transaction.fromRaw(decode(hex)); + + for (let [i, input] of tx.inputs.entries()) { + let { nonWitnessUtxo, path } = inputs[i]; + tx.updateInput(i, { nonWitnessUtxo }); + let key = child.derive(path); + let { privateKey, publicKey } = key; + let address = btc.getAddress("wpkh", privateKey, network); + tx.signIdx(privateKey, i); + } + tx.finalize(); + hex = tx.hex; + signed = true; + await tick(); + submit.click(); + }; + + let toggle = () => (submitting = !submitting); + + let { + account, + amount, + address, + message, + fee, + fees, + feeRate, + ourfee, + rates, + hex, + inputs, + } = data; $: reload(data); let reload = () => { @@ -115,10 +179,12 @@ </div> {/if} - <form method="POST" use:enhance on:submit={() => (submitting = true)}> + <form method="POST" use:enhance={handler}> <input name="pin" value={$pin} type="hidden" /> <input name="hex" value={hex} type="hidden" /> <input name="rate" value={$rate} type="hidden" /> + <input name="account" value={account.id} type="hidden" /> + <input name="signed" value={signed} type="hidden" /> <div class="flex justify-center gap-2"> <button @@ -139,6 +205,68 @@ {/if} </div> +{#if passwordPrompt} + <div + class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-20" + > + <div + class="relative top-1/3 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white space-y-5" + > + <h1 class="text-center text-2xl font-semibold"> + {$t("payments.enterWalletPass")} + </h1> + <form on:submit|preventDefault={signTx}> + <div class="relative mb-5"> + {#if revealPassword} + <input + name="password" + type="text" + required + class="bg-primary" + bind:value={password} + autocapitalize="none" + use:focus + /> + {:else} + <input + name="password" + type="password" + required + class="bg-primary" + bind:value={password} + autocapitalize="none" + use:focus + /> + {/if} + <button + type="button" + on:click={() => (revealPassword = !revealPassword)} + class="absolute right-5 top-6" + > + <Icon icon={revealPassword ? "eye" : "eye-off"} /> + </button> + </div> + <div class="w-full flex"> + <button + type="button" + class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80 mx-auto" + on:click={cancel} + on:keydown={cancel} + > + <div class="my-auto">Cancel</div> + </button> + <button + type="submit" + class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80 mx-auto" + > + <div class="my-auto">Submit</div> + </button> + </div> + </form> + </div> + </div> +{/if} + <style> .no-transition { view-transition-name: fee; diff --git a/src/routes/(app)/send/ecash/+page.svelte b/src/routes/(app)/send/ecash/+page.svelte index 15c7c5d3..615c068a 100644 --- a/src/routes/(app)/send/ecash/+page.svelte +++ b/src/routes/(app)/send/ecash/+page.svelte @@ -29,12 +29,6 @@ let toggle = () => (submitting = !submitting); </script> -<a href="/send"> - <button type="button" class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80"> - <Icon icon="arrow-left" style="w-10" /> - </button> -</a> - <div class="container px-4 max-w-xl mx-auto space-y-5 text-center"> <h1 class="text-3xl md:text-4xl font-semibold mb-2"> {$t("payments.createEcash")} diff --git a/src/routes/(app)/sent/+page.server.ts b/src/routes/(app)/sent/+page.server.ts index 62a4d9b4..4cf580ee 100644 --- a/src/routes/(app)/sent/+page.server.ts +++ b/src/routes/(app)/sent/+page.server.ts @@ -1,6 +1,7 @@ import { get, auth } from "$lib/utils"; export let load = async ({ cookies }) => { - let { payments } = await get("/payments", auth(cookies)); + let account = cookies.get("account"); + let { payments } = await get(`/payments?account=${account}`, auth(cookies)); return { payments }; }; diff --git a/src/routes/(app)/settings/[...tab]/+page.svelte b/src/routes/(app)/settings/[...tab]/+page.svelte index c495a5cc..7f4dc87e 100644 --- a/src/routes/(app)/settings/[...tab]/+page.svelte +++ b/src/routes/(app)/settings/[...tab]/+page.svelte @@ -206,7 +206,7 @@ <input type="hidden" name="pin" value={$pin} /> <input type="hidden" name="tab" value={tab} /> - <div class="mt-24 mb-20 px-3 md:px-0 w-full md:w-[400px] mx-auto space-y-8"> + <div class="mt-24 mb-20 px-3 md:px-0 w-full md:max-w-lg mx-auto space-y-8"> <div class="header"> <h1 class="text-center text-3xl md:text-4xl font-semibold mb-10"> {$t("user.settings.header")} |
