diff options
| author | Adam Soltys <[email protected]> | 2026-02-16 16:05:56 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-02-16 16:05:56 -0800 |
| commit | 95427ee5189269e992ae4c788ce356709ed92c57 (patch) | |
| tree | 675507f470d626a55ff530c8ff76cc4e8df424e0 /src | |
| parent | ac06683100e089ad589661b14ebaf5dc9582a256 (diff) | |
| download | coinos-ui-95427ee5189269e992ae4c788ce356709ed92c57.tar.gz coinos-ui-95427ee5189269e992ae4c788ce356709ed92c57.zip | |
default accounts
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/Account.svelte | 9 | ||||
| -rw-r--r-- | src/routes/(app)/+layout.svelte | 63 | ||||
| -rw-r--r-- | src/routes/(app)/account/+server.ts | 11 | ||||
| -rw-r--r-- | src/routes/(app)/account/[id]/+page.svelte | 7 | ||||
| -rw-r--r-- | src/routes/(app)/account/ark/+page.svelte | 9 | ||||
| -rw-r--r-- | src/routes/(app)/account/pass/+page.svelte | 13 | ||||
| -rw-r--r-- | src/routes/(app)/account/seed/+page.svelte | 49 | ||||
| -rw-r--r-- | src/routes/(app)/post/[...path]/+server.ts | 10 | ||||
| -rw-r--r-- | src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte | 15 | ||||
| -rw-r--r-- | src/routes/(static)/login/+page.svelte | 2 | ||||
| -rw-r--r-- | src/routes/(static)/register/+page.server.ts | 14 | ||||
| -rw-r--r-- | src/routes/(static)/register/+page.svelte | 76 |
12 files changed, 165 insertions, 113 deletions
diff --git a/src/components/Account.svelte b/src/components/Account.svelte index 5ba1bedd..953e44ab 100644 --- a/src/components/Account.svelte +++ b/src/components/Account.svelte @@ -11,7 +11,6 @@ import { fiat } from "$lib/store"; import { arkkey, arkaid } from "$lib/ark"; import { getRememberedWalletPassword, forgetWalletPassword, getCachedPrfKey } from "$lib/passwordCache"; - import { isPrfEncrypted, prfDecrypt } from "$lib/crypto"; let { user, rates, account }: any = $props(); let currency = $derived(account.currency || user.currency); @@ -31,14 +30,14 @@ let tryUnlockArk = async () => { // Try PRF key first const prfKey = getCachedPrfKey(); - if (prfKey && user.seed && isPrfEncrypted(user.seed)) { + if (prfKey) { try { const [{ HDKey }, { entropyToMnemonic, mnemonicToSeed }, { wordlist }] = await Promise.all([ import("@scure/bip32"), import("@scure/bip39"), import("@scure/bip39/wordlists/english.js"), ]); - const entropy = await prfDecrypt(prfKey, user.seed); + const entropy = new Uint8Array(prfKey); const mnemonic = entropyToMnemonic(entropy, wordlist); const s = await mnemonicToSeed(mnemonic); const master = HDKey.fromMasterSeed(s, versions); @@ -130,7 +129,7 @@ let displayType = $derived( accountType === "ark" ? $t("accounts.ark") - : seed || (user.seed && fingerprint) + : seed || fingerprint ? $t("accounts.bitcoin") : $t("accounts.custodial"), ); @@ -174,7 +173,7 @@ <div class="flex items-center justify-center w-8 h-8 leading-[0]"> {#if isArk} <img src="/images/ark.png" class="w-8 h-8 rounded-full object-cover" alt="Ark" /> - {:else if seed || (user.seed && fingerprint)} + {:else if seed || fingerprint} <iconify-icon noobserver icon="cryptocurrency-color:btc" width="32"></iconify-icon> {:else} <img src="/images/icon.png" class="w-8 h-8" alt="Coinos" /> diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 732ac61e..6f2e061d 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { getWallet, syncTransactions, settle, refresh, arkkey, arkaid, sendArk, arkSending } from "$lib/ark"; + import { getWallet, syncTransactions, settle, refresh, arkkey, arkaid, sendArk, arkSending, arkServerUrl } from "$lib/ark"; import { SvelteToast } from "@zerodevx/svelte-toast"; import { onDestroy, onMount } from "svelte"; import { close, connect, send, socket } from "$lib/socket"; @@ -22,9 +22,10 @@ import AppHeader from "$comp/AppHeader.svelte"; import Nostr from "$comp/Nostr.svelte"; import Password from "$comp/Password.svelte"; - import { s, f, toFiat, success, getCookie, warning, post } from "$lib/utils"; + import { s, f, toFiat, success, getCookie, warning, post, versions } from "$lib/utils"; import { t, locale, loading } from "$lib/translations"; import { goto, invalidate, afterNavigate, preloadData } from "$app/navigation"; + import { getCachedPrfKey } from "$lib/passwordCache"; let { data, children } = $props(); @@ -126,6 +127,62 @@ if (browser && user && $arkkey) initArk(); }); + let autoProvisionAccounts = async () => { + if (!user) return; + if (sessionStorage.getItem("accounts:provisioned")) return; + const prfKey = getCachedPrfKey(); + if (!prfKey) return; + + sessionStorage.setItem("accounts:provisioned", "1"); + + try { + const res = await fetch("/account", { headers: { accept: "application/json" } }); + const accounts = await res.json(); + + const hasBitcoin = accounts.some((a: any) => a.type === "bitcoin"); + const hasArk = accounts.some((a: any) => a.type === "ark"); + if (hasBitcoin && hasArk) return; + + const [{ HDKey }, { entropyToMnemonic, mnemonicToSeed }, { wordlist }] = await Promise.all([ + import("@scure/bip32"), + import("@scure/bip39"), + import("@scure/bip39/wordlists/english.js"), + ]); + + const entropy = new Uint8Array(prfKey); + const mnemonic = entropyToMnemonic(entropy, wordlist); + const seed = await mnemonicToSeed(mnemonic); + const master = HDKey.fromMasterSeed(seed, versions); + + if (!hasArk) { + const { SingleKey, Wallet } = await import("@arkade-os/sdk"); + const { bytesToHex } = await import("@noble/hashes/utils.js"); + const arkChild = master.derive("m/86'/0'/0'/0/0"); + const arkHex = bytesToHex(arkChild.privateKey!); + const identity = SingleKey.fromHex(arkHex); + const wallet = await Wallet.create({ identity, arkServerUrl }); + const arkAddress = await wallet.getAddress(); + await post("/account", { name: "Savings", type: "ark", arkAddress }); + } + + if (!hasBitcoin) { + const child = master.derive("m/84'/0'/0'"); + await post("/account", { + fingerprint: child.fingerprint.toString(16).padStart(8, "0"), + pubkey: child.publicExtendedKey, + name: "Vault", + type: "bitcoin", + accountIndex: 0, + }); + } + + invalidate("app:payments"); + } catch (e) { + console.error("Auto-provision accounts failed:", e); + sessionStorage.removeItem("accounts:provisioned"); + } + }; + onMount(async () => { if (browser) { checkSocket(); @@ -138,6 +195,8 @@ $arkaid = ""; localStorage.removeItem("arkkey:uid"); } + + autoProvisionAccounts(); } // if (window.NDEFReader) { diff --git a/src/routes/(app)/account/+server.ts b/src/routes/(app)/account/+server.ts index fbe1214b..4387373e 100644 --- a/src/routes/(app)/account/+server.ts +++ b/src/routes/(app)/account/+server.ts @@ -1,6 +1,15 @@ -import { auth, post } from "$lib/utils"; +import { auth, get, post } from "$lib/utils"; import { error, json } from "@sveltejs/kit"; +export async function GET({ cookies }) { + try { + return json(await get("/accounts", auth(cookies))); + } catch (e: any) { + console.log(e); + error(500, e.message); + } +} + export async function POST({ cookies, request }) { try { const body = await request.json(); diff --git a/src/routes/(app)/account/[id]/+page.svelte b/src/routes/(app)/account/[id]/+page.svelte index cfcb6dea..0bab9c1b 100644 --- a/src/routes/(app)/account/[id]/+page.svelte +++ b/src/routes/(app)/account/[id]/+page.svelte @@ -12,14 +12,13 @@ import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english.js"; import { getRememberedWalletPassword, forgetWalletPassword, getCachedPrfKey } from "$lib/passwordCache"; - import { isPrfEncrypted, prfDecrypt } from "$lib/crypto"; let { data } = $props(); let account = $derived(data.account); let user = $derived(data.user); let rates = $derived(data.rates); let id = $derived(account.id); - let seed = $derived(account.seed || (account.fingerprint && user.seed)); + let seed = $derived(account.seed || account.fingerprint); let displayType = $derived( account.type === "ark" ? $t("accounts.ark") @@ -73,9 +72,9 @@ let revealWithPrfKey = async () => { const prfKey = getCachedPrfKey(); - if (!prfKey || !seed || !isPrfEncrypted(seed)) return false; + if (!prfKey) return false; try { - const entropy = await prfDecrypt(prfKey, seed); + const entropy = new Uint8Array(prfKey); mnemonic = entropyToMnemonic(entropy, wordlist); return true; } catch (e) { diff --git a/src/routes/(app)/account/ark/+page.svelte b/src/routes/(app)/account/ark/+page.svelte index 7725f81a..f2bc8991 100644 --- a/src/routes/(app)/account/ark/+page.svelte +++ b/src/routes/(app)/account/ark/+page.svelte @@ -20,14 +20,13 @@ getCachedPrfKey, rememberPrfKey, } from "$lib/passwordCache"; - import { isPrfEncrypted, prfDecrypt } from "$lib/crypto"; import { loginWithPasskey } from "$lib/passkey"; let { data } = $props(); let user = $derived(data.user); let hasMasterSeed = $derived(!!user.seed); - let hasPrfSeed = $derived(!!user.seed && isPrfEncrypted(user.seed)); + let hasPrfSeed = $derived(!!getCachedPrfKey()); let privateKey = $state(""); let arkAddress = $state(""); @@ -43,13 +42,13 @@ let revealConfirm = $state(false); let rememberForMs = $state(defaultRememberForMs); - let name = "Ark Vault"; + let name = "Savings"; let type = "ark"; let deriveFromPrfKey = async (prfKey: ArrayBuffer) => { try { const { SingleKey, Wallet } = await import("@arkade-os/sdk"); - const entropy = await prfDecrypt(prfKey, user.seed); + const entropy = new Uint8Array(prfKey); const mnemonic = entropyToMnemonic(entropy, wordlist); const seed = await mnemonicToSeed(mnemonic); const master = HDKey.fromMasterSeed(seed, versions); @@ -225,7 +224,7 @@ <div class="space-y-5"> <div class="flex items-center justify-center gap-2"> - <h1 class="text-3xl font-semibold">Create Ark Wallet</h1> + <h1 class="text-3xl font-semibold">Create Savings Account</h1> </div> <div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-5"> diff --git a/src/routes/(app)/account/pass/+page.svelte b/src/routes/(app)/account/pass/+page.svelte index ba1e878c..23e54ac7 100644 --- a/src/routes/(app)/account/pass/+page.svelte +++ b/src/routes/(app)/account/pass/+page.svelte @@ -17,7 +17,6 @@ forgetWalletPassword, getCachedPrfKey, } from "$lib/passwordCache"; - import { isPrfEncrypted, prfEncrypt } from "$lib/crypto"; let { data }: any = $props(); @@ -36,18 +35,16 @@ return; } - // PRF users: encrypt seed and create account immediately, skip password + // PRF users: derive from PRF key directly, skip password const prfKey = getCachedPrfKey(); if (prfKey) { submitting = true; await tick(); try { - const entropy = mnemonicToEntropy($mnemonic as string, wordlist); - const encryptedSeed = await prfEncrypt(prfKey, entropy); - await post("/post/user", { seed: encryptedSeed }); - + const entropy = new Uint8Array(prfKey); + const mn = entropyToMnemonic(entropy, wordlist); const master = HDKey.fromMasterSeed( - await mnemonicToSeed($mnemonic as string), + await mnemonicToSeed(mn), versions, ); const child = master.derive("m/84'/0'/0'"); @@ -56,7 +53,7 @@ await post("/account", { fingerprint, pubkey, - name: $t("accounts.savings"), + name: "Vault", type: "bitcoin", accountIndex: 0, }); diff --git a/src/routes/(app)/account/seed/+page.svelte b/src/routes/(app)/account/seed/+page.svelte index 994a4422..adfd5573 100644 --- a/src/routes/(app)/account/seed/+page.svelte +++ b/src/routes/(app)/account/seed/+page.svelte @@ -9,7 +9,6 @@ import { wordlist } from "@scure/bip39/wordlists/english.js"; import { HDKey } from "@scure/bip32"; import { goto } from "$app/navigation"; - import { isPrfEncrypted, prfDecrypt } from "$lib/crypto"; import { getCachedPrfKey } from "$lib/passwordCache"; import Spinner from "$comp/Spinner.svelte"; @@ -19,33 +18,29 @@ onMount(async () => { if (!browser) return; - if (user.seed && isPrfEncrypted(user.seed)) { - const prfKey = getCachedPrfKey(); - if (prfKey) { - creating = true; - try { - const entropy = await prfDecrypt(prfKey, user.seed); - const mn = entropyToMnemonic(entropy, wordlist); - const seed = await mnemonicToSeed(mn); - const master = HDKey.fromMasterSeed(seed, versions); - const child = master.derive("m/84'/0'/0'"); - const pubkey = child.publicExtendedKey; - const fingerprint = child.fingerprint.toString(16).padStart(8, "0"); - await post("/account", { - fingerprint, - pubkey, - name: $t("accounts.savings"), - type: "bitcoin", - accountIndex: 0, - }); - goto(`/${user.username}`); - } catch (e: any) { - console.log(e); - fail(e.message); - creating = false; - } - } else { + const prfKey = getCachedPrfKey(); + if (prfKey) { + creating = true; + try { + const entropy = new Uint8Array(prfKey); + const mn = entropyToMnemonic(entropy, wordlist); + const seed = await mnemonicToSeed(mn); + const master = HDKey.fromMasterSeed(seed, versions); + const child = master.derive("m/84'/0'/0'"); + const pubkey = child.publicExtendedKey; + const fingerprint = child.fingerprint.toString(16).padStart(8, "0"); + await post("/account", { + fingerprint, + pubkey, + name: "Vault", + type: "bitcoin", + accountIndex: 0, + }); goto(`/${user.username}`); + } catch (e: any) { + console.log(e); + fail(e.message); + creating = false; } return; } diff --git a/src/routes/(app)/post/[...path]/+server.ts b/src/routes/(app)/post/[...path]/+server.ts index 244683ee..2c3e6d18 100644 --- a/src/routes/(app)/post/[...path]/+server.ts +++ b/src/routes/(app)/post/[...path]/+server.ts @@ -3,10 +3,18 @@ import { auth } from "$lib/utils"; export async function POST({ cookies, fetch, params, request }) { const { path } = params; + const headers: Record<string, string> = { "Content-Type": "application/json" }; + + const authHeader = request.headers.get("authorization"); + if (authHeader) { + headers.authorization = authHeader; + } else { + Object.assign(headers, auth(cookies)); + } return fetch(`${base}/${path}`, { method: "POST", body: JSON.stringify(await request.json()), - headers: { "Content-Type": "application/json", ...auth(cookies) }, + headers, }); } 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 b62d2ec9..e14d41ba 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte @@ -14,7 +14,6 @@ import { hex as hexUtil } from "@scure/base"; import * as btc from "@scure/btc-signer"; import { getRememberedWalletPassword, forgetWalletPassword, getCachedPrfKey } from "$lib/passwordCache"; - import { isPrfEncrypted, prfDecrypt } from "$lib/crypto"; import { sendArkViaForward } from "$lib/ark"; import Amount from "$comp/Amount.svelte"; @@ -30,12 +29,10 @@ let error = $state(""); let trySignWithCachedPassword = async () => { - let seed = account.seed || data.user.seed; - if (seed && isPrfEncrypted(seed)) { - const prfKey = getCachedPrfKey(); - if (!prfKey) return false; + const prfKey = getCachedPrfKey(); + if (prfKey) { try { - await signTxWithPrf(prfKey, seed); + await signTxWithPrf(prfKey); return true; } catch (e: any) { return false; @@ -86,7 +83,7 @@ return; } - if ((account.seed || (data.user.seed && account.fingerprint)) && !signed) (cancel(), void togglePassword()); + if ((account.seed || account.fingerprint) && !signed) (cancel(), void togglePassword()); return async ({ result }) => { if (result.type === "redirect") { goto(result.location); @@ -97,7 +94,7 @@ }; }; - let signTxWithPrf = async (prfKey: ArrayBuffer, encryptedSeed: string) => { + let signTxWithPrf = async (prfKey: ArrayBuffer) => { const [{ Transaction }, { HDKey }, { entropyToMnemonic, mnemonicToSeed }, { wordlist }] = await Promise.all([ import("@scure/btc-signer"), @@ -105,7 +102,7 @@ import("@scure/bip39"), import("@scure/bip39/wordlists/english.js"), ]); - let entropy = await prfDecrypt(prfKey, encryptedSeed); + let entropy = new Uint8Array(prfKey); let mnemonic = entropyToMnemonic(entropy, wordlist); let seed = await mnemonicToSeed(mnemonic); let master = HDKey.fromMasterSeed(seed, network as any); diff --git a/src/routes/(static)/login/+page.svelte b/src/routes/(static)/login/+page.svelte index 1859af6f..fc1a6921 100644 --- a/src/routes/(static)/login/+page.svelte +++ b/src/routes/(static)/login/+page.svelte @@ -126,7 +126,7 @@ const result = deserialize(await response.text()); - if (result.type === "success") { + if (result.type === "success" || result.type === "redirect") { rememberPrfKey(prfKey, defaultRememberForMs); await invalidateAll(); } diff --git a/src/routes/(static)/register/+page.server.ts b/src/routes/(static)/register/+page.server.ts index e34bcbae..6986e890 100644 --- a/src/routes/(static)/register/+page.server.ts +++ b/src/routes/(static)/register/+page.server.ts @@ -15,9 +15,9 @@ export const actions = { register: async ({ request }) => { const ip = request.headers.get("cf-connecting-ip"); const form = await fd(request); - const { picture, username, challenge, recaptcha, pubkey } = form; + const { picture, username, challenge, recaptcha } = form; - const user = { picture, username, challenge, recaptcha, pubkey }; + const user = { picture, username, challenge, recaptcha }; const headers: Record<string, string> = {}; if (ip) headers["cf-connecting-ip"] = ip; @@ -32,7 +32,7 @@ export const actions = { activate: async ({ cookies, request }) => { const form = await fd(request); - const { token, username, sk } = form; + const { token, username, sk, pubkey } = form; let { loginRedirect } = form; if (loginRedirect === "undefined") loginRedirect = undefined; @@ -55,6 +55,14 @@ export const actions = { }); } + if (pubkey) { + try { + await post("/user", { pubkey }, { authorization: `Bearer ${token}` }); + } catch (e) { + console.log("Failed to update pubkey:", e); + } + } + redirect(303, loginRedirect || `/${username}`); }, }; diff --git a/src/routes/(static)/register/+page.svelte b/src/routes/(static)/register/+page.svelte index 0652b1a8..273d319a 100644 --- a/src/routes/(static)/register/+page.svelte +++ b/src/routes/(static)/register/+page.svelte @@ -15,7 +15,6 @@ import { focus, fail, versions, post } from "$lib/utils"; import { registerPasskey } from "$lib/passkey"; - import { prfEncrypt, isPrfEncrypted } from "$lib/crypto"; import { rememberPrfKey, defaultRememberForMs } from "$lib/passwordCache"; import { avatar, signer, password, pin, loginRedirect } from "$lib/store"; import { t } from "$lib/translations"; @@ -53,11 +52,7 @@ let needsPasskey = $state(false); // Stored between handleSubmit and retryPasskey - let pendingMnemonic: string | undefined; - let pendingSkHex: string | undefined; let pendingToken: string | undefined; - let pendingSk: string | undefined; - let pendingLoginRedirect: string | undefined; let cleared; let clear = () => { @@ -130,31 +125,6 @@ return; } - // Generate mnemonic and derive Nostr pubkey (NIP-06) - const [ - { generateMnemonic, mnemonicToSeed, mnemonicToEntropy }, - { wordlist }, - { HDKey }, - { getPublicKey }, - { bytesToHex }, - ] = await Promise.all([ - import("@scure/bip39"), - import("@scure/bip39/wordlists/english.js"), - import("@scure/bip32"), - import("nostr-tools"), - import("@noble/hashes/utils.js"), - ]); - - const mnemonic = await generateMnemonic(wordlist); - pendingMnemonic = mnemonic; - const seed = await mnemonicToSeed(mnemonic); - const master = HDKey.fromMasterSeed(seed, versions); - const nostrChild = master.derive("m/44'/1237'/0'/0/0"); - const sk = nostrChild.privateKey!; - const pubkey = getPublicKey(sk); - pendingSkHex = bytesToHex(sk); - data.set("pubkey", pubkey); - data.set("picture", `${$page.url.origin}/api/public/${punks[index]}.webp`); if ($avatar) { try { @@ -186,11 +156,8 @@ if (result.type === "success") { // User created but no cookies set yet — must complete passkey first - const { token, sk: serverSk } = (result as any).data; + const { token } = (result as any).data; pendingToken = token; - pendingSk = serverSk; - pendingLoginRedirect = - data.get("loginRedirect") as string || `/${username}`; let prfKey: ArrayBuffer; try { @@ -211,35 +178,50 @@ } async function activateSession(prfKey: ArrayBuffer) { - if (!pendingMnemonic) return; - - const { mnemonicToEntropy } = await import("@scure/bip39"); - const { wordlist } = await import("@scure/bip39/wordlists/english.js"); - const entropy = mnemonicToEntropy(pendingMnemonic, wordlist); - const encryptedSeed = await prfEncrypt(prfKey, entropy); rememberPrfKey(prfKey, defaultRememberForMs); - // Activate session — sets auth cookies server-side + // Derive Nostr key from PRF key + const [ + { entropyToMnemonic, mnemonicToSeed }, + { wordlist }, + { HDKey }, + { getPublicKey }, + { bytesToHex }, + ] = await Promise.all([ + import("@scure/bip39"), + import("@scure/bip39/wordlists/english.js"), + import("@scure/bip32"), + import("nostr-tools"), + import("@noble/hashes/utils.js"), + ]); + + const entropy = new Uint8Array(prfKey); + const mnemonic = entropyToMnemonic(entropy, wordlist); + const seed = await mnemonicToSeed(mnemonic); + const master = HDKey.fromMasterSeed(seed, versions); + const nostrChild = master.derive("m/44'/1237'/0'/0/0"); + const sk = nostrChild.privateKey!; + const pubkey = getPublicKey(sk); + const skHex = bytesToHex(sk); + + // Activate session — sets auth cookies and updates pubkey server-side const activateData = new FormData(); activateData.set("token", pendingToken!); activateData.set("username", username!); - if (pendingSk) activateData.set("sk", pendingSkHex || pendingSk); + activateData.set("sk", skHex); + activateData.set("pubkey", pubkey); const activateResponse = await fetch("/register?/activate", { method: "POST", body: activateData, }); - // Cookies are now set — save the encrypted seed before navigating - await post("/post/user", { seed: encryptedSeed }); - + const activateResult = deserialize(await activateResponse.text()); await invalidateAll(); - const activateResult = deserialize(await activateResponse.text()); if (activateResult.type === "redirect") { applyAction(activateResult); } else { - // Fallback: navigate manually applyAction({ type: "redirect", status: 303, location: `/${username}` }); } } |
