From b22c813f9fb3b4d69dade75d22e90e6779919f07 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 16 Feb 2026 20:56:39 -0800 Subject: nostr key derivation --- src/routes/(app)/+layout.svelte | 38 +++----- src/routes/(app)/[username]/(profile)/+page.svelte | 2 +- src/routes/(app)/account/[id]/+page.svelte | 103 ++++++++++++++++----- src/routes/(app)/account/ark/+page.svelte | 92 ++++++++++++------ src/routes/(app)/account/bitcoin/+page.server.ts | 5 + src/routes/(app)/account/import/+page.svelte | 2 +- src/routes/(app)/account/new/+page.server.ts | 8 +- src/routes/(app)/account/pass/+page.svelte | 15 +-- src/routes/(app)/account/seed/+page.svelte | 16 ++-- src/routes/(app)/account/setup/+page.svelte | 2 +- .../[address]/[amount]/[...feeRate]/+page.svelte | 13 ++- src/routes/(app)/settings/security/+page.svelte | 4 +- src/routes/(static)/login/+page.svelte | 10 ++ src/routes/(static)/register/+page.svelte | 47 ++++++++-- 14 files changed, 247 insertions(+), 110 deletions(-) create mode 100644 src/routes/(app)/account/bitcoin/+page.server.ts (limited to 'src/routes') diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 6f2e061d..0fde24fa 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -130,7 +130,11 @@ let autoProvisionAccounts = async () => { if (!user) return; if (sessionStorage.getItem("accounts:provisioned")) return; - const prfKey = getCachedPrfKey(); + let prfKey = getCachedPrfKey(); + if (!prfKey) { + const { getWalletEntropy } = await import("$lib/walletEntropy"); + prfKey = await getWalletEntropy(); + } if (!prfKey) return; sessionStorage.setItem("accounts:provisioned", "1"); @@ -139,9 +143,8 @@ 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; + if (hasArk) return; const [{ HDKey }, { entropyToMnemonic, mnemonicToSeed }, { wordlist }] = await Promise.all([ import("@scure/bip32"), @@ -154,27 +157,14 @@ 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, - }); - } + 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: $t("accounts.savings"), type: "ark", arkAddress }); invalidate("app:payments"); } catch (e) { diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte index 4885189b..b9af8756 100644 --- a/src/routes/(app)/[username]/(profile)/+page.svelte +++ b/src/routes/(app)/[username]/(profile)/+page.svelte @@ -64,7 +64,7 @@ {#each accounts as account} {/each} - +

{$t("user.settings.thresholdDesc")}

- -
- - -

{$t("user.settings.reserveDesc")}

-
{/if} + +
{#if mnemonic} @@ -224,17 +255,45 @@ {/if} + {#if account.type === "bitcoin"} + {#if importing} +
+ +
+ + +
+
+ {:else} + + {/if} + {/if} + {#if seed || account.type === "ark"} {/if} +
- @@ -254,7 +313,7 @@ bind:submit={doneReserve} bind:element={thresholdEl} /> - + {/if} @@ -270,7 +329,7 @@ bind:submit={doneReserve} bind:element={reserveEl} /> - + {/if} diff --git a/src/routes/(app)/account/ark/+page.svelte b/src/routes/(app)/account/ark/+page.svelte index f2bc8991..b6bec5b7 100644 --- a/src/routes/(app)/account/ark/+page.svelte +++ b/src/routes/(app)/account/ark/+page.svelte @@ -26,7 +26,8 @@ let user = $derived(data.user); let hasMasterSeed = $derived(!!user.seed); - let hasPrfSeed = $derived(!!getCachedPrfKey()); + let hasPrfSeed = $state(!!getCachedPrfKey()); + let hasSigner = $state(false); let privateKey = $state(""); let arkAddress = $state(""); @@ -42,7 +43,7 @@ let revealConfirm = $state(false); let rememberForMs = $state(defaultRememberForMs); - let name = "Savings"; + let name = $t("accounts.savings"); let type = "ark"; let deriveFromPrfKey = async (prfKey: ArrayBuffer) => { @@ -60,7 +61,19 @@ confirmed = true; } catch (e: any) { console.log("PRF seed derivation failed", e); - fail("Failed to decrypt seed"); + fail($t("accounts.failedToDecryptSeed")); + } + }; + + let unlockWithNostr = async () => { + try { + const { deriveNostrEntropy } = await import("$lib/walletEntropy"); + const entropy = await deriveNostrEntropy(); + hasPrfSeed = true; + await deriveFromPrfKey(entropy); + needsPasskeyUnlock = false; + } catch (e: any) { + fail(e.message || $t("accounts.passkeyUnlockFailed")); } }; @@ -72,16 +85,29 @@ await deriveFromPrfKey(prfKey); needsPasskeyUnlock = false; } else { - fail("Passkey does not support PRF"); + fail($t("accounts.passkeyNoPrf")); } } catch (e: any) { if (e.name !== "NotAllowedError") { - fail(e.message || "Passkey unlock failed"); + fail(e.message || $t("accounts.passkeyUnlockFailed")); } } }; onMount(async () => { + hasSigner = !!localStorage.getItem("signer"); + + // Try wallet entropy (covers both cached prfKey and silent nsec derivation) + if (!hasPrfSeed) { + const { getWalletEntropy } = await import("$lib/walletEntropy"); + const entropy = await getWalletEntropy(); + if (entropy) { + hasPrfSeed = true; + await deriveFromPrfKey(entropy); + return; + } + } + if (hasPrfSeed) { const prfKey = getCachedPrfKey(); if (prfKey) { @@ -117,7 +143,7 @@ confirmed = true; } catch (e: any) { console.log("master seed derivation failed", e); - fail("Invalid password"); + fail($t("accounts.invalidPassword")); } }; @@ -164,7 +190,7 @@ if (hasMasterSeed) { if (!password) { - fail("Password required"); + fail($t("accounts.passwordRequired")); return; } @@ -190,7 +216,7 @@ } if (!confirmed) { - fail("Please backup your key first"); + fail($t("accounts.pleaseBackupFirst")); return; } @@ -224,7 +250,7 @@
-

Create Savings Account

+

{$t("accounts.createSavingsAccount")}

@@ -232,27 +258,33 @@ {#if needsPasskeyUnlock}

- Your Ark wallet will be derived from your master seed. Unlock with your passkey to continue. + {$t("accounts.arkUnlockWithPasskey")}

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

- Your Ark wallet will be derived from your passkey-protected master seed. + {$t("accounts.arkDerivedFromPasskey")}

- +
@@ -261,7 +293,7 @@ {:else if hasMasterSeed}

- Your Ark wallet will be derived from your master seed. Enter your wallet password to continue. + {$t("accounts.arkEnterPassword")}

{/if} diff --git a/src/routes/(app)/account/setup/+page.svelte b/src/routes/(app)/account/setup/+page.svelte index faab0938..35c0c9d9 100644 --- a/src/routes/(app)/account/setup/+page.svelte +++ b/src/routes/(app)/account/setup/+page.svelte @@ -17,7 +17,7 @@
- +
Savings account
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 e14d41ba..1a0df2f2 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte @@ -38,6 +38,17 @@ return false; } } + // Try silent Nostr derivation + const { getWalletEntropy } = await import("$lib/walletEntropy"); + const entropy = await getWalletEntropy(); + if (entropy) { + try { + await signTxWithPrf(entropy); + return true; + } catch (e: any) { + return false; + } + } const cached = getRememberedWalletPassword(); if (!cached) return false; try { @@ -76,7 +87,7 @@ goto(`/sent/${p.id}`, { invalidateAll: true }); } catch (e: any) { loading = false; - error = e.message || "Failed to send"; + error = e.message || $t("payments.failedToSend"); } })(); diff --git a/src/routes/(app)/settings/security/+page.svelte b/src/routes/(app)/settings/security/+page.svelte index 9666b193..33981645 100644 --- a/src/routes/(app)/settings/security/+page.svelte +++ b/src/routes/(app)/settings/security/+page.svelte @@ -215,12 +215,12 @@ {/if} {#if confirming2fa || disabling2fa} - + {/if}
- +
-- cgit v1.2.3