From e5406ef66f2e3f43e45fcf92401f6f4b87dce796 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 26 Feb 2026 21:28:50 -0800 Subject: ui tweaks --- src/components/Account.svelte | 144 ++------ src/locales/en.json | 5 + src/routes/(app)/account/[id]/+page.svelte | 115 +++--- src/routes/(app)/account/[id]/seed/+page.server.ts | 6 + src/routes/(app)/account/[id]/seed/+page.svelte | 82 +++++ src/routes/(app)/account/ark/+page.svelte | 384 ++------------------- src/routes/(app)/account/pass/+page.svelte | 207 ++--------- src/routes/(app)/payment/[id]/+page.svelte | 73 ++-- .../[address]/[amount]/[...feeRate]/+page.svelte | 97 ++---- 9 files changed, 271 insertions(+), 842 deletions(-) create mode 100644 src/routes/(app)/account/[id]/seed/+page.server.ts create mode 100644 src/routes/(app)/account/[id]/seed/+page.svelte (limited to 'src') diff --git a/src/components/Account.svelte b/src/components/Account.svelte index 6f3d48d8..45ffdd8b 100644 --- a/src/components/Account.svelte +++ b/src/components/Account.svelte @@ -1,7 +1,5 @@ + +
+

{$t("accounts.seedPhrase")}

+ +
+ {#if mnemonic} +

+ Derivation path: {derivationPath} +

+ + + + + {/if} + + +
+
diff --git a/src/routes/(app)/account/ark/+page.svelte b/src/routes/(app)/account/ark/+page.svelte index 243931fe..398cfc71 100644 --- a/src/routes/(app)/account/ark/+page.svelte +++ b/src/routes/(app)/account/ark/+page.svelte @@ -3,20 +3,15 @@ import { tick, onMount } from "svelte"; import { t } from "$lib/translations"; import Spinner from "$comp/Spinner.svelte"; - import { bytesToHex, randomBytes } from "@noble/hashes/utils.js"; - import { hex } from "@scure/base"; - import { focus, fail, post, copy, versions } from "$lib/utils"; + import { bytesToHex } from "@noble/hashes/utils.js"; + import { fail, post, versions } from "$lib/utils"; import { goto } from "$app/navigation"; import { arkServerUrl } from "$lib/ark"; import { HDKey } from "@scure/bip32"; import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english.js"; import { - rememberForOptions, defaultRememberForMs, - rememberWalletPassword, - forgetWalletPassword, - getRememberedWalletPassword, getCachedPrfKey, rememberPrfKey, } from "$lib/passwordCache"; @@ -25,24 +20,13 @@ let { data } = $props(); let user = $derived(data.user); - let hasMasterSeed = $derived(!!user.seed); let hasPrfSeed = $state(!!getCachedPrfKey()); let hasSigner = $state(false); - let privateKey = $state(""); let arkAddress = $state(""); - let nsec = $state(""); - let showNsec = $state(false); - let confirmed = $state(false); let submitting = $state(false); let needsPasskeyUnlock = $state(false); - let password = $state(""); - let confirm = $state(""); - let revealPassword = $state(false); - let revealConfirm = $state(false); - let rememberForMs = $state(defaultRememberForMs); - let name = $t("accounts.savings"); let type = "ark"; @@ -58,7 +42,6 @@ const identity = SingleKey.fromHex(arkHex); const wallet = await Wallet.create({ identity, arkServerUrl }); arkAddress = await wallet.getAddress(); - confirmed = true; } catch (e: any) { console.log("PRF seed derivation failed", e); fail($t("accounts.failedToDecryptSeed")); @@ -115,135 +98,26 @@ } else { needsPasskeyUnlock = true; } - } else if (hasMasterSeed) { - const cached = getRememberedWalletPassword(); - if (cached) { - password = cached; - confirm = cached; - await deriveFromMasterSeed(cached); - } } else { - await generateKey(); + needsPasskeyUnlock = true; } }); - let deriveFromMasterSeed = async (pw) => { - try { - const { decrypt } = await import("nostr-tools/nip49"); - const { SingleKey, Wallet } = await import("@arkade-os/sdk"); - let entropy = await decrypt(user.seed, pw); - let mnemonic = entropyToMnemonic(entropy, wordlist); - let seed = await mnemonicToSeed(mnemonic, pw); - let master = HDKey.fromMasterSeed(seed, versions); - let arkChild = master.derive("m/86'/0'/0'/0/0"); - let arkHex = bytesToHex(arkChild.privateKey!); - const identity = SingleKey.fromHex(arkHex); - const wallet = await Wallet.create({ identity, arkServerUrl }); - arkAddress = await wallet.getAddress(); - confirmed = true; - } catch (e: any) { - console.log("master seed derivation failed", e); - fail($t("accounts.invalidPassword")); - } - }; - - let generateKey = async () => { - const { nip19 } = await import("nostr-tools"); - const { SingleKey, Wallet } = await import("@arkade-os/sdk"); - privateKey = bytesToHex(randomBytes(32)); - nsec = nip19.nsecEncode(hex.decode(privateKey)); - showNsec = false; - confirmed = false; - - const identity = SingleKey.fromHex(privateKey); - const wallet = await Wallet.create({ identity, arkServerUrl }); - arkAddress = await wallet.getAddress(); - }; - - let revealNsec = () => { - showNsec = true; - }; - - let confirmBackup = () => { - confirmed = true; - }; - let submit = async () => { - if (hasPrfSeed) { - submitting = true; - await tick(); - try { - if (!arkAddress) { - const prfKey = getCachedPrfKey(); - if (prfKey) await deriveFromPrfKey(prfKey); - else throw new Error("PRF key not available"); - } - await post("/account", { name, type, arkAddress }); - goto(`/${user.username}`); - } catch (e: any) { - console.log(e); - fail(e.message); - } - submitting = false; - return; - } - - if (hasMasterSeed) { - if (!password) { - fail($t("accounts.passwordRequired")); - return; - } - - submitting = true; - await tick(); - - try { - if (!arkAddress) await deriveFromMasterSeed(password); - await post("/account", { name, type, arkAddress }); - if (rememberForMs) { - rememberWalletPassword(password, rememberForMs); - } else { - forgetWalletPassword(); - } - goto(`/${user.username}`); - } catch (e: any) { - console.log(e); - fail(e.message); - } - - submitting = false; - return; - } - - if (!confirmed) { - fail($t("accounts.pleaseBackupFirst")); - return; - } - - if (!password || password !== confirm) { - fail($t("accounts.passwordMismatch")); - return; - } - submitting = true; await tick(); - try { - const { encrypt } = await import("nostr-tools/nip49"); - let seed = await encrypt(hex.decode(privateKey), password); - - await post("/account", { name, seed, type, arkAddress }); - if (rememberForMs) { - rememberWalletPassword(password, rememberForMs); - } else { - forgetWalletPassword(); + if (!arkAddress) { + const prfKey = getCachedPrfKey(); + if (prfKey) await deriveFromPrfKey(prfKey); + else throw new Error("PRF key not available"); } + await post("/account", { name, type, arkAddress }); goto(`/${user.username}`); } catch (e: any) { console.log(e); fail(e.message); } - submitting = false; }; @@ -254,246 +128,36 @@
- {#if hasPrfSeed} - {#if needsPasskeyUnlock} -
-

- {$t("accounts.arkUnlockWithPasskey")} -

- {#if hasSigner} - - {/if} - -
- {:else} -
-

- {$t("accounts.arkDerivedFromPasskey")} -

-
- - - - -
-
- {/if} - {:else if hasMasterSeed} -
-

- {$t("accounts.arkEnterPassword")} -

- - - -
- - -
- -
- - - - -
-
- {:else if !confirmed} -
+ {#if needsPasskeyUnlock} +

- {$t("accounts.arkSecuredByKey")} + {$t("accounts.arkUnlockWithPasskey")}

- - {#if showNsec} -
- -
- -
- -
- {:else} - {/if} +
{:else}

- {$t("accounts.setPasswordToEncrypt")} + {$t("accounts.arkDerivedFromPasskey")}

- - - - - -
- - -
-
- + + +
diff --git a/src/routes/(app)/account/pass/+page.svelte b/src/routes/(app)/account/pass/+page.svelte index 2e1c711f..2a327a88 100644 --- a/src/routes/(app)/account/pass/+page.svelte +++ b/src/routes/(app)/account/pass/+page.svelte @@ -1,232 +1,69 @@ -
-
-

{$t("accounts.accountPassword")}

+{#if submitting} +
+
- - -

- {$t("accounts.passwordWarning")} -

- - - - - -
- - -
- -
- - - - -
- -
+{/if} diff --git a/src/routes/(app)/payment/[id]/+page.svelte b/src/routes/(app)/payment/[id]/+page.svelte index ca3d5b6a..80474a55 100644 --- a/src/routes/(app)/payment/[id]/+page.svelte +++ b/src/routes/(app)/payment/[id]/+page.svelte @@ -95,40 +95,41 @@ {/snippet} {#snippet copyable(label, value, href)} -
- {label} -
- - - {#if href} - - - - {/if} + + {#if href} + e.stopPropagation()} aria-label="View in explorer"> + + + {/if} +
+ {#if expanded.has(label)} +
+ {value} +
+ {/if}
{/snippet} @@ -153,7 +154,7 @@ {/if}
- {format(new Date(created), "MMM d, yyyy", { locale })} · {format(new Date(created), "h:mmaaa", { locale })} + {format(new Date(created), "MMM d, yyyy", { locale })}{format(new Date(created), "h:mmaaa", { locale })}
@@ -167,7 +168,7 @@ {/if} -
+
{#if tip} {@render field("payments.amount", a)} {@render field("invoice.tip", tip)} @@ -215,8 +216,8 @@
{:else if memo} -
- {$t("payments.memo")} +
+ {$t("payments.memo")}
{#if memo.includes("text/plain")} {JSON.parse(memo)[0][1]} 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 4cb22155..3d4dc139 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte @@ -4,71 +4,51 @@ import { enhance } from "$app/forms"; import Toggle from "$comp/Toggle.svelte"; import Spinner from "$comp/Spinner.svelte"; - import WalletPass from "$comp/WalletPass.svelte"; import { page } from "$app/stores"; - import { toFiat, f, focus, loc, s, sat, closest, network } from "$lib/utils"; + import { toFiat, f, fail, focus, loc, 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 { getRememberedWalletPassword, forgetWalletPassword, getCachedPrfKey } from "$lib/passwordCache"; + import { getCachedPrfKey } from "$lib/passwordCache"; import { sendArkViaForward } from "$lib/ark"; + import { prfDecrypt, isPrfEncrypted } from "$lib/crypto"; import Amount from "$comp/Amount.svelte"; let { data, form }: any = $props(); let { encode, decode } = hexUtil; - let passwordPrompt = $state(); - let password: any = $state(); - let cancel = $state(() => (passwordPrompt = false)); let signed = $state(); let loading = $state(false); let error = $state(""); - let trySignWithCachedPassword = async () => { + let trySign = async () => { const prfKey = getCachedPrfKey(); if (prfKey) { try { await signTxWithPrf(prfKey); - return true; - } catch (e: any) { - return false; - } + return; + } catch (e: any) {} } - // Try silent Nostr derivation - const { getWalletEntropy } = await import("$lib/walletEntropy"); + const { getWalletEntropy, deriveNostrEntropy } = await import("$lib/walletEntropy"); const entropy = await getWalletEntropy(); if (entropy) { try { await signTxWithPrf(entropy); - return true; - } catch (e: any) { - return false; - } + return; + } catch (e: any) {} } - const cached = getRememberedWalletPassword(); - if (!cached) return false; try { - password = cached; - await signTx(); - return true; + const interactiveEntropy = await deriveNostrEntropy(); + await signTxWithPrf(interactiveEntropy); } catch (e: any) { - forgetWalletPassword(); - return false; + error = e.message || $t("payments.failedToSend"); } }; - let togglePassword = async () => { - if (passwordPrompt) { - passwordPrompt = false; - return; - } - if (await trySignWithCachedPassword()) return; - passwordPrompt = true; - }; let handler = ({ cancel }) => { if (account.type === "ark") { cancel(); @@ -94,7 +74,7 @@ return; } - if ((account.seed || account.fingerprint) && !signed) (cancel(), void togglePassword()); + if ((account.seed || account.fingerprint) && !signed) (cancel(), void trySign()); return async ({ result }) => { if (result.type === "redirect") { goto(result.location); @@ -113,46 +93,15 @@ import("@scure/bip39"), import("@scure/bip39/wordlists/english.js"), ]); - let entropy = new Uint8Array(prfKey).slice(0, 16); - let mnemonic = entropyToMnemonic(entropy, wordlist); - let seed = await mnemonicToSeed(mnemonic); - let master = HDKey.fromMasterSeed(seed, network as any); - let child = master.derive(`m/84'/0'/${account.accountIndex ?? 0}'`); - - let raw = decode(hex); - let isPSBT = raw[0] === 0x70 && raw[1] === 0x73 && raw[2] === 0x62 && raw[3] === 0x74; - let tx: any = isPSBT ? Transaction.fromPSBT(raw) : Transaction.fromRaw(raw); - - for (let [i, input] of tx.inputs.entries()) { - let { witnessUtxo, path } = inputs[i]; - if (!isPSBT) { - witnessUtxo.amount = BigInt(witnessUtxo.amount); - witnessUtxo.script = decode(witnessUtxo.script); - tx.updateInput(i, { witnessUtxo }); - } - let key = child.derive(path); - tx.signIdx(key.privateKey!, i); + let mnemonicStr: string; + if (account.seed && isPrfEncrypted(account.seed)) { + const entropy = await prfDecrypt(prfKey, account.seed); + mnemonicStr = entropyToMnemonic(entropy, wordlist); + } else { + const entropy = new Uint8Array(prfKey).slice(0, 16); + mnemonicStr = entropyToMnemonic(entropy, wordlist); } - - tx.finalize(); - hex = tx.hex; - signed = true; - await tick(); - (submit as any).click(); - }; - - let signTx = async () => { - const { decrypt } = await import("nostr-tools/nip49"); - const [{ Transaction }, { HDKey }, { entropyToMnemonic, mnemonicToSeed }, { wordlist }] = - await Promise.all([ - import("@scure/btc-signer"), - import("@scure/bip32"), - import("@scure/bip39"), - import("@scure/bip39/wordlists/english.js"), - ]); - let entropy = await decrypt(account.seed || data.user.seed, password as string); - let mnemonic = entropyToMnemonic(entropy, wordlist); - let seed = await mnemonicToSeed(mnemonic, password as string); + let seed = await mnemonicToSeed(mnemonicStr); let master = HDKey.fromMasterSeed(seed, network as any); let child = master.derive(`m/84'/0'/${account.accountIndex ?? 0}'`); @@ -285,10 +234,6 @@ {/if}
-{#if passwordPrompt} - -{/if} -