aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-26 21:28:50 -0800
committerAdam Soltys <[email protected]>2026-02-26 21:28:50 -0800
commite5406ef66f2e3f43e45fcf92401f6f4b87dce796 (patch)
tree25927ce2b130973aceac13e0e8f679b180ea5cad /src
parent18df062a099811355cafc2024e30a7c2b68c7984 (diff)
downloadcoinos-ui-e5406ef66f2e3f43e45fcf92401f6f4b87dce796.tar.gz
coinos-ui-e5406ef66f2e3f43e45fcf92401f6f4b87dce796.zip
ui tweaks
Diffstat (limited to 'src')
-rw-r--r--src/components/Account.svelte144
-rw-r--r--src/locales/en.json5
-rw-r--r--src/routes/(app)/account/[id]/+page.svelte115
-rw-r--r--src/routes/(app)/account/[id]/seed/+page.server.ts6
-rw-r--r--src/routes/(app)/account/[id]/seed/+page.svelte82
-rw-r--r--src/routes/(app)/account/ark/+page.svelte384
-rw-r--r--src/routes/(app)/account/pass/+page.svelte207
-rw-r--r--src/routes/(app)/payment/[id]/+page.svelte73
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte97
9 files changed, 271 insertions, 842 deletions
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 @@
<script lang="ts">
- import { hex } from "@scure/base";
import { bytesToHex } from "@noble/hashes/utils.js";
- import WalletPass from "$comp/WalletPass.svelte";
import { run } from "svelte/legacy";
import { onMount } from "svelte";
import { goto } from "$app/navigation";
@@ -10,7 +8,7 @@
import { versions, s, f, loc, toFiat } from "$lib/utils";
import { fiat, importing } from "$lib/store";
import { arkkey, arkaid } from "$lib/ark";
- import { getRememberedWalletPassword, forgetWalletPassword, getCachedPrfKey } from "$lib/passwordCache";
+ import { prfDecrypt, isPrfEncrypted } from "$lib/crypto";
let { user, rates, account }: any = $props();
let currency = $derived(account.currency || user.currency);
@@ -19,90 +17,48 @@
let arkBalance = $state(0);
let arkLoading = $state(false);
- let passwordPrompt = $state(false);
- let password: string | undefined = $state();
- let pendingUrl: string | undefined = $state();
- let cancel = $state(() => {
- passwordPrompt = false;
- pendingUrl = undefined;
- });
let tryUnlockArk = async () => {
- // Try PRF key first
- const prfKey = getCachedPrfKey();
- if (prfKey) {
+ const deriveArkKey = async (key: ArrayBuffer) => {
+ const [{ HDKey }, { entropyToMnemonic, mnemonicToSeed }, { wordlist }] = await Promise.all([
+ import("@scure/bip32"),
+ import("@scure/bip39"),
+ import("@scure/bip39/wordlists/english.js"),
+ ]);
+ let mnemonicStr: string;
+ if (account.seed && isPrfEncrypted(account.seed)) {
+ const ent = await prfDecrypt(key, account.seed);
+ mnemonicStr = entropyToMnemonic(ent, wordlist);
+ } else {
+ const entropy = new Uint8Array(key).slice(0, 16);
+ mnemonicStr = entropyToMnemonic(entropy, wordlist);
+ }
+ const s = await mnemonicToSeed(mnemonicStr);
+ const master = HDKey.fromMasterSeed(s, versions);
+ const arkChild = master.derive("m/86'/0'/0'/0/0");
+ $arkkey = bytesToHex(arkChild.privateKey!);
+ $arkaid = id;
+ };
+
+ // Try cached PRF key or silent nsec
+ const { getWalletEntropy, deriveNostrEntropy } = await import("$lib/walletEntropy");
+ const entropy = await getWalletEntropy();
+ if (entropy) {
try {
- 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).slice(0, 16);
- const mnemonic = entropyToMnemonic(entropy, wordlist);
- const s = await mnemonicToSeed(mnemonic);
- const master = HDKey.fromMasterSeed(s, versions);
- const arkChild = master.derive("m/86'/0'/0'/0/0");
- $arkkey = bytesToHex(arkChild.privateKey!);
- $arkaid = id;
+ await deriveArkKey(entropy);
return true;
} catch (e) {
- console.log("PRF unlock failed", e);
+ console.log("Wallet entropy unlock failed", e);
}
}
- // Try silent Nostr derivation
- if (!prfKey) {
- const { getWalletEntropy } = await import("$lib/walletEntropy");
- const nostrKey = await getWalletEntropy();
- if (nostrKey) {
- try {
- const [{ HDKey }, { entropyToMnemonic, mnemonicToSeed }, { wordlist }] = await Promise.all([
- import("@scure/bip32"),
- import("@scure/bip39"),
- import("@scure/bip39/wordlists/english.js"),
- ]);
- const entropy = new Uint8Array(nostrKey).slice(0, 16);
- const mnemonic = entropyToMnemonic(entropy, wordlist);
- const s = await mnemonicToSeed(mnemonic);
- const master = HDKey.fromMasterSeed(s, versions);
- const arkChild = master.derive("m/86'/0'/0'/0/0");
- $arkkey = bytesToHex(arkChild.privateKey!);
- $arkaid = id;
- return true;
- } catch (e) {
- console.log("Nostr entropy unlock failed", e);
- }
- }
- }
-
- // Then try cached wallet password
- const cached = getRememberedWalletPassword();
- if (!cached) return false;
+ // Try interactive nostr derivation
try {
- const { decrypt } = await import("nostr-tools/nip49");
- if (seed) {
- // Legacy: per-account seed is raw hex key
- $arkkey = hex.encode(decrypt(seed, cached) as Uint8Array);
- } else if (user.seed) {
- // Master seed: derive m/86'/0'/0'/0/0
- const [{ HDKey }, { entropyToMnemonic, mnemonicToSeed }, { wordlist }] = await Promise.all([
- import("@scure/bip32"),
- import("@scure/bip39"),
- import("@scure/bip39/wordlists/english.js"),
- ]);
- let entropy = await decrypt(user.seed, cached);
- let mnemonic = entropyToMnemonic(entropy, wordlist);
- let s = await mnemonicToSeed(mnemonic, cached);
- let master = HDKey.fromMasterSeed(s, versions);
- let arkChild = master.derive("m/86'/0'/0'/0/0");
- $arkkey = bytesToHex(arkChild.privateKey!);
- } else {
- return false;
- }
- $arkaid = id;
+ const interactiveEntropy = await deriveNostrEntropy();
+ await deriveArkKey(interactiveEntropy);
return true;
} catch (e) {
- forgetWalletPassword();
+ console.log("Interactive nostr unlock failed", e);
return false;
}
};
@@ -112,13 +68,10 @@
event.stopPropagation();
document.cookie = `aid=${id}; path=/; max-age=86400`;
if (isArk) {
- pendingUrl = url;
if ($arkkey && $arkaid === id) {
- goto(pendingUrl!);
+ goto(url);
} else if (await tryUnlockArk()) {
- goto(pendingUrl!);
- } else {
- passwordPrompt = true;
+ goto(url);
}
} else {
$arkkey = undefined;
@@ -126,31 +79,6 @@
}
};
- let submitPassword = async () => {
- const { decrypt } = await import("nostr-tools/nip49");
- if (seed) {
- $arkkey = hex.encode(decrypt(seed, password!) as Uint8Array);
- } else if (user.seed) {
- const [{ HDKey }, { entropyToMnemonic, mnemonicToSeed }, { wordlist }] = await Promise.all([
- import("@scure/bip32"),
- import("@scure/bip39"),
- import("@scure/bip39/wordlists/english.js"),
- ]);
- let entropy = await decrypt(user.seed, password!);
- let mnemonic = entropyToMnemonic(entropy, wordlist);
- let s = await mnemonicToSeed(mnemonic, password!);
- let master = HDKey.fromMasterSeed(s, versions);
- let arkChild = master.derive("m/86'/0'/0'/0/0");
- $arkkey = bytesToHex(arkChild.privateKey!);
- }
- $arkaid = id;
- passwordPrompt = false;
- if (pendingUrl) {
- goto(pendingUrl);
- pendingUrl = undefined;
- }
- };
-
let displayType = $derived(
accountType === "ark"
? $t("accounts.ark")
@@ -249,7 +177,3 @@
</a>
</div>
</div>
-
-{#if passwordPrompt}
- <WalletPass bind:password bind:cancel submit={submitPassword} />
-{/if}
diff --git a/src/locales/en.json b/src/locales/en.json
index 3babc54a..196c55df 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -51,8 +51,13 @@
"copy": "Copy",
"copyNwc": "Copy NWC",
"revealMnemonic": "Reveal seed phrase",
+ "viewMnemonic": "View seed phrase",
+ "hideMnemonic": "Hide seed phrase",
"regenerate": "Regenerate",
"deleteAccount": "Delete account",
+ "deleteConfirm": "Are you sure you want to delete this account? This action cannot be undone.",
+ "delete": "Delete",
+ "cancel": "Cancel",
"addAccount": "Add account",
"create": "Create",
"import": "Import",
diff --git a/src/routes/(app)/account/[id]/+page.svelte b/src/routes/(app)/account/[id]/+page.svelte
index c8085c09..0d652479 100644
--- a/src/routes/(app)/account/[id]/+page.svelte
+++ b/src/routes/(app)/account/[id]/+page.svelte
@@ -1,20 +1,19 @@
<script lang="ts">
import { page } from "$app/stores";
- import Mnemonic from "$comp/Mnemonic.svelte";
import Numpad from "$comp/Numpad.svelte";
import Toggle from "$comp/Toggle.svelte";
- import WalletPass from "$comp/WalletPass.svelte";
import { goto } from "$app/navigation";
- import { copy, fail, focus, loc, post } from "$lib/utils";
+ import { fail, focus, loc, post } from "$lib/utils";
import { importing } from "$lib/store";
import { enhance } from "$app/forms";
import { t } from "$lib/translations";
import { tick } from "svelte";
- import { entropyToMnemonic, mnemonicToSeed, validateMnemonic, mnemonicToEntropy } from "@scure/bip39";
+ import { mnemonicToSeed, validateMnemonic, mnemonicToEntropy } from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english.js";
import { HDKey } from "@scure/bip32";
import { versions } from "$lib/utils";
- import { getRememberedWalletPassword, forgetWalletPassword, getCachedPrfKey } from "$lib/passwordCache";
+ import { getCachedPrfKey } from "$lib/passwordCache";
+ import { prfEncrypt } from "$lib/crypto";
import Spinner from "$comp/Spinner.svelte";
let { data } = $props();
@@ -49,12 +48,6 @@
let rate = $derived(rates[currency]);
let fiats = $derived(Object.keys(rates).sort((a, b) => a.localeCompare(b)));
- let mnemonic: string | undefined = $state(),
- password: string | undefined = $state();
- let passwordPrompt = $state();
- let cancel = $state(() => (passwordPrompt = false));
- let toggle = () => (passwordPrompt = !passwordPrompt);
-
let editingReserve = $state(false),
editingThreshold = $state(false),
doneReserve = $state(),
@@ -96,6 +89,17 @@
importSubmitting = true;
try {
+ const { getWalletEntropy } = await import("$lib/walletEntropy");
+ let prfKey = getCachedPrfKey() || await getWalletEntropy();
+ if (!prfKey) {
+ fail($t("accounts.failedToDecryptSeed"));
+ importSubmitting = false;
+ return;
+ }
+
+ let entropy = mnemonicToEntropy(text, wordlist);
+ let seed = await prfEncrypt(prfKey, entropy);
+
let s = await mnemonicToSeed(text);
let master = HDKey.fromMasterSeed(s, versions);
let child = master.derive(`m/84'/0'/${account.accountIndex ?? 0}'`);
@@ -105,6 +109,7 @@
await post(`/post/account/${id}`, {
fingerprint: fp,
pubkey,
+ seed,
});
$importing = new Set([...$importing, id]);
@@ -117,54 +122,7 @@
importSubmitting = false;
};
- let revealWithPrfKey = async () => {
- let prfKey = getCachedPrfKey();
- if (!prfKey) {
- const { getWalletEntropy } = await import("$lib/walletEntropy");
- prfKey = await getWalletEntropy();
- }
- if (!prfKey) return false;
- try {
- const entropy = new Uint8Array(prfKey).slice(0, 16);
- mnemonic = entropyToMnemonic(entropy, wordlist);
- return true;
- } catch (e) {
- return false;
- }
- };
-
- let revealWithCachedPassword = async () => {
- const cached = getRememberedWalletPassword();
- if (!cached) return false;
- try {
- const { decrypt } = await import("nostr-tools/nip49");
- let entropy = await decrypt(seed, cached);
- mnemonic = entropyToMnemonic(entropy, wordlist);
- password = cached;
- return true;
- } catch (e: any) {
- forgetWalletPassword();
- return false;
- }
- };
-
- let requestMnemonic = async () => {
- if (await revealWithPrfKey()) return;
- if (await revealWithCachedPassword()) return;
- toggle();
- };
-
- let submit = $state(async () => {
- passwordPrompt = false;
- try {
- const { decrypt } = await import("nostr-tools/nip49");
- let entropy = await decrypt(seed, password as string);
- mnemonic = entropyToMnemonic(entropy, wordlist);
- } catch (e: any) {
- fail($t("accounts.invalidPasswordTryAgain"));
- passwordPrompt = true;
- }
- });
+ let showDeleteConfirm = $state(false);
let del = async () => {
try {
@@ -258,17 +216,10 @@
</button>
<div class="space-y-2">
- {#if mnemonic}
- <Mnemonic {mnemonic} />
-
- <button onclick={() => copy(mnemonic as string)} type="button" class="btn">
- <iconify-icon noobserver icon="ph:copy-bold" width="32"></iconify-icon>
- <div class="my-auto">{$t("accounts.copy")}</div>
- </button>
- {:else if seed}
- <button onclick={requestMnemonic} type="button" class="btn">
+ {#if seed || account.type === "ark"}
+ <button onclick={() => goto(`/account/${id}/seed`)} type="button" class="btn">
<iconify-icon noobserver icon="ph:eye-bold" width="32"></iconify-icon>
- <div class="my-auto">{$t("accounts.revealMnemonic")}</div>
+ <div class="my-auto">{$t("accounts.viewMnemonic")}</div>
</button>
{/if}
@@ -298,8 +249,8 @@
{/if}
{#if seed || account.type === "ark"}
- <button onclick={del} type="button" class="btn">
- <iconify-icon noobserver icon="ph:trash-bold" width="32"></iconify-icon>
+ <button onclick={() => (showDeleteConfirm = true)} type="button" class="btn">
+ <iconify-icon noobserver icon="ph:trash-bold" width="32" class="text-error"></iconify-icon>
<div class="my-auto">{$t("accounts.deleteAccount")}</div>
</button>
{/if}
@@ -310,10 +261,6 @@
</div>
</div>
-{#if passwordPrompt}
- <WalletPass bind:password bind:cancel {submit} />
-{/if}
-
{#if editingThreshold}
<div class="fixed bg-base-100/90 inset-0 overflow-y-auto h-full w-full z-50 max-w-lg mx-auto">
<div class="relative p-5 border shadow-lg rounded-md bg-base-100 space-y-5">
@@ -347,3 +294,21 @@
</div>
</div>
{/if}
+
+{#if showDeleteConfirm}
+ <div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" onclick={() => (showDeleteConfirm = false)}>
+ <div class="bg-base-100 rounded-2xl p-6 max-w-sm w-full space-y-4 text-center" onclick={(e) => e.stopPropagation()}>
+ <iconify-icon noobserver icon="ph:warning-bold" width="48" class="text-error"></iconify-icon>
+ <h2 class="text-xl font-semibold">{$t("accounts.deleteAccount")}</h2>
+ <p class="text-secondary">{$t("accounts.deleteConfirm")}</p>
+ <div class="flex gap-2">
+ <button type="button" class="btn !w-auto grow" onclick={() => (showDeleteConfirm = false)}>
+ {$t("accounts.cancel")}
+ </button>
+ <button type="button" class="btn btn-error !w-auto grow" onclick={del}>
+ {$t("accounts.delete")}
+ </button>
+ </div>
+ </div>
+ </div>
+{/if}
diff --git a/src/routes/(app)/account/[id]/seed/+page.server.ts b/src/routes/(app)/account/[id]/seed/+page.server.ts
new file mode 100644
index 00000000..ec271ef5
--- /dev/null
+++ b/src/routes/(app)/account/[id]/seed/+page.server.ts
@@ -0,0 +1,6 @@
+import { auth, get } from "$lib/utils";
+
+export const load = async ({ cookies, params }) => {
+ const account = await get(`/account/${params.id}`, auth(cookies));
+ return { account };
+};
diff --git a/src/routes/(app)/account/[id]/seed/+page.svelte b/src/routes/(app)/account/[id]/seed/+page.svelte
new file mode 100644
index 00000000..3b7f8c56
--- /dev/null
+++ b/src/routes/(app)/account/[id]/seed/+page.svelte
@@ -0,0 +1,82 @@
+<script lang="ts">
+ import { page } from "$app/stores";
+ import Mnemonic from "$comp/Mnemonic.svelte";
+ import { goto } from "$app/navigation";
+ import { copy, fail } from "$lib/utils";
+ import { t } from "$lib/translations";
+ import { entropyToMnemonic } from "@scure/bip39";
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
+ import { getCachedPrfKey } from "$lib/passwordCache";
+ import { prfDecrypt, isPrfEncrypted } from "$lib/crypto";
+
+ let { data } = $props();
+ let account = $derived(data.account);
+ let id = $derived(account.id);
+
+ let mnemonic: string | undefined = $state();
+
+ let derivationPath = $derived(
+ account.type === "ark"
+ ? "m/86'/0'/0'/0/0"
+ : `m/84'/0'/${account.accountIndex ?? 0}'`,
+ );
+
+ let revealWithPrfKey = async () => {
+ let prfKey = getCachedPrfKey();
+ if (!prfKey) {
+ const { getWalletEntropy } = await import("$lib/walletEntropy");
+ prfKey = await getWalletEntropy();
+ }
+ if (!prfKey) return false;
+ try {
+ if (account.seed && isPrfEncrypted(account.seed)) {
+ const entropy = await prfDecrypt(prfKey, account.seed);
+ mnemonic = entropyToMnemonic(entropy, wordlist);
+ } else {
+ const entropy = new Uint8Array(prfKey).slice(0, 16);
+ mnemonic = entropyToMnemonic(entropy, wordlist);
+ }
+ return true;
+ } catch (e) {
+ return false;
+ }
+ };
+
+ let requestMnemonic = async () => {
+ if (await revealWithPrfKey()) return;
+ try {
+ const { deriveNostrEntropy } = await import("$lib/walletEntropy");
+ const entropy = await deriveNostrEntropy();
+ const ent = new Uint8Array(entropy).slice(0, 16);
+ mnemonic = entropyToMnemonic(ent, wordlist);
+ } catch (e) {
+ fail($t("accounts.failedToDecryptSeed"));
+ }
+ };
+
+ requestMnemonic();
+</script>
+
+<div class="space-y-5">
+ <h1 class="text-center text-3xl font-semibold">{$t("accounts.seedPhrase")}</h1>
+
+ <div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-4">
+ {#if mnemonic}
+ <p class="text-center text-secondary">
+ Derivation path: <span class="font-mono font-bold">{derivationPath}</span>
+ </p>
+
+ <Mnemonic {mnemonic} />
+
+ <button onclick={() => copy(mnemonic as string)} type="button" class="btn">
+ <iconify-icon noobserver icon="ph:copy-bold" width="32"></iconify-icon>
+ <div class="my-auto">{$t("accounts.copy")}</div>
+ </button>
+ {/if}
+
+ <button onclick={() => goto(`/account/${id}`)} type="button" class="btn btn-primary">
+ <iconify-icon noobserver icon="ph:arrow-left-bold" width="32"></iconify-icon>
+ <div class="my-auto">{$t("accounts.back")}</div>
+ </button>
+ </div>
+</div>
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;
};
</script>
@@ -254,246 +128,36 @@
</div>
<div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-5">
- {#if hasPrfSeed}
- {#if needsPasskeyUnlock}
- <div class="space-y-5">
- <p class="text-secondary">
- {$t("accounts.arkUnlockWithPasskey")}
- </p>
- {#if hasSigner}
- <button type="button" class="btn btn-accent" onclick={unlockWithNostr}>
- <img src="/images/nostr.png" class="w-6" alt="Nostr" />
- {$t("accounts.unlockWithNostr")}
- </button>
- {/if}
- <button type="button" class="btn btn-accent" onclick={unlockWithPasskey}>
- <iconify-icon noobserver icon="ph:fingerprint-bold" width="24"></iconify-icon>
- {$t("accounts.unlockWithPasskey")}
- </button>
- </div>
- {:else}
- <form onsubmit={preventDefault(submit)} class="space-y-5">
- <p class="text-secondary">
- {$t("accounts.arkDerivedFromPasskey")}
- </p>
- <div class="flex gap-2">
- <a href={`/${user.username}`} class="contents">
- <button type="button" class="btn !w-auto grow">{$t("accounts.back")}</button>
- </a>
- <button disabled={submitting} type="submit" class="btn btn-accent !w-auto grow">
- {#if submitting}
- <Spinner />
- {:else}
- {$t("accounts.createWallet")}
- {/if}
- </button>
- </div>
- </form>
- {/if}
- {:else if hasMasterSeed}
- <form onsubmit={preventDefault(submit)} class="space-y-5">
- <p class="text-secondary">
- {$t("accounts.arkEnterPassword")}
- </p>
-
- <label for="password" class="input flex items-center justify-center gap-2 w-full">
- {#if revealPassword}
- <input
- name="password"
- type="text"
- required
- bind:value={password}
- autocapitalize="none"
- class="clean"
- placeholder={$t("payments.enterWalletPass")}
- />
- {:else}
- <input
- use:focus
- name="password"
- type="password"
- placeholder={$t("payments.enterWalletPass")}
- required
- bind:value={password}
- autocapitalize="none"
- class="clean"
- />
- {/if}
-
- <button
- type="button"
- class="ml-auto"
- aria-label="Toggle password visibility"
- onclick={() => (revealPassword = !revealPassword)}
- >
- <iconify-icon
- noobserver
- icon={revealPassword ? "ph:eye-bold" : "ph:eye-slash-bold"}
- width="32"
- ></iconify-icon>
- </button>
- </label>
-
- <div class="space-y-2">
- <label for="rememberFor" class="text-sm text-secondary">{$t("user.settings.rememberFor")}</label>
- <select
- id="rememberFor"
- class="w-full"
- value={rememberForMs}
- onchange={(e) => (rememberForMs = Number((e.target as HTMLSelectElement).value))}
- >
- {#each rememberForOptions as option}
- <option value={option.ms}>{$t(option.label)}</option>
- {/each}
- </select>
- </div>
-
- <div class="flex gap-2">
- <a href={`/${user.username}`} class="contents">
- <button type="button" class="btn !w-auto grow">Back</button>
- </a>
- <button disabled={submitting} type="submit" class="btn btn-accent !w-auto grow">
- {#if submitting}
- <Spinner />
- {:else}
- Create Wallet
- {/if}
- </button>
- </div>
- </form>
- {:else if !confirmed}
- <div class="space-y-4">
+ {#if needsPasskeyUnlock}
+ <div class="space-y-5">
<p class="text-secondary">
- {$t("accounts.arkSecuredByKey")}
+ {$t("accounts.arkUnlockWithPasskey")}
</p>
-
- {#if showNsec}
- <div class="space-y-2">
- <button
- type="button"
- class="break-all p-4 rounded-lg cursor-pointer border text-left w-full"
- onclick={() => copy(nsec)}
- aria-label="Copy backup key"
- >
- {nsec}
- </button>
- </div>
-
- <div class="flex gap-2">
- <button type="button" class="btn btn-accent flex-grow" onclick={confirmBackup}>
- <iconify-icon noobserver icon="ph:check-bold" width="24"></iconify-icon>
- {$t("accounts.backedItUp")}
- </button>
- </div>
- {:else}
- <button type="button" class="btn btn-warning w-full" onclick={revealNsec}>
- <iconify-icon noobserver icon="ph:eye-bold" width="24"></iconify-icon>
- {$t("accounts.revealBackupKey")}
+ {#if hasSigner}
+ <button type="button" class="btn btn-accent" onclick={unlockWithNostr}>
+ <img src="/images/nostr.png" class="w-6" alt="Nostr" />
+ {$t("accounts.unlockWithNostr")}
</button>
{/if}
+ <button type="button" class="btn btn-accent" onclick={unlockWithPasskey}>
+ <iconify-icon noobserver icon="ph:fingerprint-bold" width="24"></iconify-icon>
+ {$t("accounts.unlockWithPasskey")}
+ </button>
</div>
{:else}
<form onsubmit={preventDefault(submit)} class="space-y-5">
<p class="text-secondary">
- {$t("accounts.setPasswordToEncrypt")}
+ {$t("accounts.arkDerivedFromPasskey")}
</p>
-
- <label for="password" class="input flex items-center justify-center gap-2 w-full">
- {#if revealPassword}
- <input
- name="password"
- type="text"
- required
- bind:value={password}
- autocapitalize="none"
- class="clean"
- placeholder={$t("accounts.password")}
- />
- {:else}
- <input
- use:focus
- name="password"
- type="password"
- placeholder={$t("accounts.password")}
- required
- bind:value={password}
- autocapitalize="none"
- class="clean"
- />
- {/if}
-
- <button
- type="button"
- class="ml-auto"
- aria-label="Toggle password visibility"
- onclick={() => (revealPassword = !revealPassword)}
- >
- <iconify-icon
- noobserver
- icon={revealPassword ? "ph:eye-bold" : "ph:eye-slash-bold"}
- width="32"
- ></iconify-icon>
- </button>
- </label>
-
- <label for="confirm" class="input flex items-center justify-center gap-2 w-full">
- {#if revealConfirm}
- <input
- name="confirm"
- type="text"
- placeholder={$t("accounts.confirmPassword")}
- required
- bind:value={confirm}
- autocapitalize="none"
- class="clean"
- />
- {:else}
- <input
- type="password"
- placeholder={$t("accounts.confirmPassword")}
- required
- bind:value={confirm}
- autocapitalize="none"
- class="clean"
- />
- {/if}
- <button
- type="button"
- class="ml-auto"
- aria-label="Toggle confirmation visibility"
- onclick={() => (revealConfirm = !revealConfirm)}
- >
- <iconify-icon
- noobserver
- icon={revealConfirm ? "ph:eye-bold" : "ph:eye-slash-bold"}
- width="32"
- ></iconify-icon>
- </button>
- </label>
-
- <div class="space-y-2">
- <label for="rememberFor" class="text-sm text-secondary">{$t("user.settings.rememberFor")}</label>
- <select
- id="rememberFor"
- class="w-full"
- value={rememberForMs}
- onchange={(e) => (rememberForMs = Number((e.target as HTMLSelectElement).value))}
- >
- {#each rememberForOptions as option}
- <option value={option.ms}>{$t(option.label)}</option>
- {/each}
- </select>
- </div>
-
<div class="flex gap-2">
- <button type="button" class="btn !w-auto grow" onclick={() => (confirmed = false)}>
- {$t("accounts.back")}
- </button>
+ <a href={`/${user.username}`} class="contents">
+ <button type="button" class="btn !w-auto grow">{$t("accounts.back")}</button>
+ </a>
<button disabled={submitting} type="submit" class="btn btn-accent !w-auto grow">
{#if submitting}
<Spinner />
{:else}
- Create Wallet
+ {$t("accounts.createWallet")}
{/if}
</button>
</div>
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 @@
<script lang="ts">
- import { preventDefault } from "svelte/legacy";
-
import { tick, onMount } from "svelte";
import { t } from "$lib/translations";
import { mnemonic } from "$lib/store";
import Spinner from "$comp/Spinner.svelte";
- import { mnemonicToSeed, mnemonicToEntropy, entropyToMnemonic } from "@scure/bip39";
+ import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english.js";
- import { focus, versions, fail, post } from "$lib/utils";
+ import { versions, fail, post } from "$lib/utils";
import { goto } from "$app/navigation";
import { HDKey } from "@scure/bip32";
- import {
- rememberForOptions,
- defaultRememberForMs,
- rememberWalletPassword,
- forgetWalletPassword,
- getCachedPrfKey,
- } from "$lib/passwordCache";
let { data }: any = $props();
let user = $derived(data.user);
let submitting = $state(false);
- let confirm: string = $state(""),
- password: string = $state(""),
- revealPassword = $state(),
- revealConfirm = $state();
- let rememberForMs = $state(defaultRememberForMs);
-
onMount(async () => {
if (!$mnemonic) {
goto("/account/seed");
return;
}
- // PRF/Nostr users: derive from entropy directly, skip password
- const { getWalletEntropy } = await import("$lib/walletEntropy");
- const prfKey = await getWalletEntropy();
- if (prfKey) {
- submitting = true;
- await tick();
+ const { getWalletEntropy, deriveNostrEntropy } = await import("$lib/walletEntropy");
+ let prfKey = await getWalletEntropy();
+ if (!prfKey) {
try {
- const entropy = new Uint8Array(prfKey).slice(0, 16);
- const mn = entropyToMnemonic(entropy, wordlist);
- const master = HDKey.fromMasterSeed(
- await mnemonicToSeed(mn),
- 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.vault"),
- type: "bitcoin",
- accountIndex: 0,
- });
-
- $mnemonic = "";
+ prfKey = await deriveNostrEntropy();
+ } catch (e) {
+ fail($t("accounts.failedToDecryptSeed"));
goto(`/${user.username}`);
- } catch (e: any) {
- console.log(e);
- fail(e.message);
+ return;
}
- submitting = false;
}
- });
- let submit = async () => {
submitting = true;
await tick();
- if (!password || password !== confirm) {
- fail($t("accounts.passwordMismatch"));
- return;
- }
-
try {
- const { encrypt } = await import("nostr-tools/nip49");
- let seed = await encrypt(mnemonicToEntropy($mnemonic as string, wordlist), password);
-
- // Save master seed on user
- await post("/post/user", { seed });
-
- let master = HDKey.fromMasterSeed(
- await mnemonicToSeed($mnemonic as string, password),
+ const entropy = new Uint8Array(prfKey).slice(0, 16);
+ const mn = entropyToMnemonic(entropy, wordlist);
+ const master = HDKey.fromMasterSeed(
+ await mnemonicToSeed(mn),
versions,
);
-
- // Bitcoin account at m/84'/0'/0'
- let child = master.derive("m/84'/0'/0'");
- let pubkey = child.publicExtendedKey;
- let fingerprint = child.fingerprint.toString(16).padStart(8, "0");
+ 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"),
+ name: $t("accounts.vault"),
type: "bitcoin",
accountIndex: 0,
});
- if (rememberForMs) {
- rememberWalletPassword(password, rememberForMs);
- } else {
- forgetWalletPassword();
- }
$mnemonic = "";
goto(`/${user.username}`);
} catch (e: any) {
console.log(e);
fail(e.message);
}
-
submitting = false;
- };
+ });
</script>
-<div class="space-y-5">
- <div>
- <h1 class="text-center text-3xl font-semibold">{$t("accounts.accountPassword")}</h1>
+{#if submitting}
+ <div class="flex justify-center py-20">
+ <Spinner />
</div>
-
- <form
- onsubmit={preventDefault(submit)}
- class="container w-full mx-auto text-lg px-4 max-w-xl space-y-5"
- >
- <p class="text-secondary p-4 bg-warning/80 rounded-2xl border-warning border-2">
- {$t("accounts.passwordWarning")}
- </p>
-
- <label for="password" class="input flex items-center justify-center gap-2 w-full">
- {#if revealPassword}
- <input
- name="password"
- type="text"
- required
- bind:value={password}
- autocapitalize="none"
- class="clean"
- />
- {:else}
- <input
- use:focus
- name="password"
- type="password"
- placeholder={$t("accounts.password")}
- required
- bind:value={password}
- autocapitalize="none"
- class="clean"
- />
- {/if}
-
- <button
- type="button"
- class="ml-auto"
- aria-label="Toggle password visibility"
- onclick={() => (revealPassword = !revealPassword)}
- >
- <iconify-icon
- noobserver
- icon={revealPassword ? "ph:eye-bold" : "ph:eye-slash-bold"}
- width="32"
- ></iconify-icon>
- </button>
- </label>
-
- <label for="confirm" class="input flex items-center justify-center gap-2 w-full">
- {#if revealConfirm}
- <input
- name="confirm"
- type="text"
- placeholder={$t("accounts.confirmPassword")}
- required
- bind:value={confirm}
- autocapitalize="none"
- class="clean"
- />
- {:else}
- <input
- type="password"
- placeholder={$t("accounts.confirmPassword")}
- required
- bind:value={confirm}
- autocapitalize="none"
- class="clean"
- />
- {/if}
- <button
- type="button"
- class="ml-auto"
- aria-label="Toggle confirmation visibility"
- onclick={() => (revealConfirm = !revealConfirm)}
- >
- <iconify-icon
- noobserver
- icon={revealConfirm ? "ph:eye-bold" : "ph:eye-slash-bold"}
- width="32"
- ></iconify-icon>
- </button>
- </label>
-
- <div class="space-y-2">
- <label for="rememberFor" class="text-sm text-secondary">{$t("user.settings.rememberFor")}</label>
- <select
- id="rememberFor"
- class="w-full"
- value={rememberForMs}
- onchange={(e) => (rememberForMs = Number((e.target as HTMLSelectElement).value))}
- >
- {#each rememberForOptions as option}
- <option value={option.ms}>{$t(option.label)}</option>
- {/each}
- </select>
- </div>
-
- <div class="flex gap-2">
- <a href={`/account/seed`} class="contents">
- <button type="button" class="btn !w-auto grow">{$t("accounts.back")}</button>
- </a>
- <button disabled={submitting} type="submit" class="btn btn-accent !w-auto grow">
- {#if submitting}
- <Spinner />
- {:else}
- {$t("accounts.finish")}
- {/if}
- </button>
- </div>
- </form>
-</div>
+{/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)}
- <div class="flex justify-between items-center gap-4 py-4 border-b border-base-300 min-h-20">
- <span class="text-secondary shrink-0">{label}</span>
- <div class="flex items-center gap-2 min-w-0">
- <button
- class="font-mono text-sm text-right break-all hover:opacity-80"
- onclick={() => {
- if (expanded.has(label)) {
- expanded.delete(label);
- } else {
- expanded.add(label);
- }
- expanded = new Set(expanded);
- }}
- aria-label={expanded.has(label) ? "Collapse" : "Expand"}
- >
- {#if expanded.has(label)}
- {value}
- {:else}
- {value.slice(0, 8)}...{value.slice(-8)}
+ {@const toggle = () => { if (expanded.has(label)) expanded.delete(label); else expanded.add(label); expanded = new Set(expanded); }}
+ <div
+ class="py-4 border-b border-base-300 min-h-20 cursor-pointer"
+ onclick={toggle}
+ role="button"
+ tabindex="0"
+ onkeydown={(e) => e.key === 'Enter' && toggle()}
+ >
+ <div class="flex justify-between items-center gap-4">
+ <span class="text-secondary shrink-0">{label}</span>
+ <div class="flex items-center gap-2 min-w-0">
+ {#if !expanded.has(label)}
+ <span class="text-right break-all">
+ {value.slice(0, 8)}...{value.slice(-8)}
+ </span>
{/if}
- </button>
- <button
- class="shrink-0 hover:opacity-80"
- onclick={() => copy(value)}
- aria-label="Copy {label}"
- >
- <iconify-icon noobserver icon="ph:copy-bold" width="20" class="text-secondary"></iconify-icon>
- </button>
- {#if href}
- <a {href} target="_blank" rel="noreferrer" class="shrink-0 hover:opacity-80" aria-label="View in explorer">
- <iconify-icon noobserver icon="ph:arrow-square-out-bold" width="20" class="text-secondary"></iconify-icon>
- </a>
- {/if}
+ <button
+ class="shrink-0 hover:opacity-80"
+ onclick={(e) => { e.stopPropagation(); copy(value); }}
+ aria-label="Copy {label}"
+ >
+ <iconify-icon noobserver icon="ph:copy-bold" width="28" class="text-secondary"></iconify-icon>
+ </button>
+ {#if href}
+ <a {href} target="_blank" rel="noreferrer" class="shrink-0 hover:opacity-80" onclick={(e) => e.stopPropagation()} aria-label="View in explorer">
+ <iconify-icon noobserver icon="ph:arrow-square-out-bold" width="20" class="text-secondary"></iconify-icon>
+ </a>
+ {/if}
+ </div>
</div>
+ {#if expanded.has(label)}
+ <div class="break-all mt-2">
+ {value}
+ </div>
+ {/if}
</div>
{/snippet}
@@ -153,7 +154,7 @@
{/if}
</button>
<div class="text-secondary">
- {format(new Date(created), "MMM d, yyyy", { locale })} &middot; {format(new Date(created), "h:mmaaa", { locale })}
+ {format(new Date(created), "MMM d, yyyy", { locale })}<span class="mx-3">{format(new Date(created), "h:mmaaa", { locale })}</span>
</div>
</div>
@@ -167,7 +168,7 @@
</a>
{/if}
- <div class="mt-2">
+ <div class="mt-2 [&>*:last-child]:border-b-0">
{#if tip}
{@render field("payments.amount", a)}
{@render field("invoice.tip", tip)}
@@ -215,8 +216,8 @@
</a>
</div>
{:else if memo}
- <div class="flex justify-between items-start gap-4 py-3 border-b border-base-300">
- <span class="text-sm text-secondary shrink-0">{$t("payments.memo")}</span>
+ <div class="flex justify-between items-center gap-4 py-4 border-b border-base-300 min-h-20">
+ <span class="text-secondary shrink-0">{$t("payments.memo")}</span>
<div class="text-right break-all">
{#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}
</div>
-{#if passwordPrompt}
- <WalletPass bind:password {cancel} submit={signTx} />
-{/if}
-
<style>
.no-transition {
view-transition-name: fee;