aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-05 09:34:30 -0800
committerAdam Soltys <[email protected]>2026-02-11 15:34:32 -0800
commitec304f8bd1ffef69a85b75da033fbccbe6bfc72f (patch)
tree664e765f3d7e9c4f26463760ed155dabca76486b /src
parent59fbd17aad12acfa19eb730c4ede67d50962ba9b (diff)
downloadcoinos-ui-ec304f8bd1ffef69a85b75da033fbccbe6bfc72f.tar.gz
coinos-ui-ec304f8bd1ffef69a85b75da033fbccbe6bfc72f.zip
fix ark payment notifications
Diffstat (limited to 'src')
-rw-r--r--src/components/Account.svelte28
-rw-r--r--src/components/WalletPass.svelte33
-rw-r--r--src/lib/passwordCache.ts61
-rw-r--r--src/routes/(app)/+layout.svelte28
-rw-r--r--src/routes/(app)/account/[id]/+page.svelte38
-rw-r--r--src/routes/(app)/account/ark/+page.svelte28
-rw-r--r--src/routes/(app)/account/pass/+page.svelte28
-rw-r--r--src/routes/(app)/invoice/[id]/tip/+page.svelte8
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte28
9 files changed, 259 insertions, 21 deletions
diff --git a/src/components/Account.svelte b/src/components/Account.svelte
index 04ff97ae..aaef428b 100644
--- a/src/components/Account.svelte
+++ b/src/components/Account.svelte
@@ -6,6 +6,10 @@
import { goto } from "$app/navigation"; import Balance from "$comp/Balance.svelte";
import { t } from "$lib/translations";
import { arkkey } from "$lib/ark";
+ import {
+ getRememberedWalletPassword,
+ forgetWalletPassword,
+ } from "$lib/passwordCache";
let { user, rate, account } = $props();
let {
@@ -27,14 +31,32 @@
pendingUrl = undefined;
});
- let setAccount = (event, url) => {
+ let tryUnlockArk = async () => {
+ const cached = getRememberedWalletPassword();
+ if (!cached) return false;
+ try {
+ const { decrypt } = await import("nostr-tools/nip49");
+ $arkkey = hex.encode(decrypt(seed, cached));
+ return true;
+ } catch (e) {
+ forgetWalletPassword();
+ return false;
+ }
+ };
+
+ let setAccount = async (event, url) => {
event.preventDefault();
event.stopPropagation();
document.cookie = `aid=${id}; path=/; max-age=86400`;
if (isArk) {
pendingUrl = url;
- if ($arkkey) goto(pendingUrl);
- else passwordPrompt = true;
+ if ($arkkey) {
+ goto(pendingUrl);
+ } else if (await tryUnlockArk()) {
+ goto(pendingUrl);
+ } else {
+ passwordPrompt = true;
+ }
} else {
$arkkey = undefined;
goto(url);
diff --git a/src/components/WalletPass.svelte b/src/components/WalletPass.svelte
index e3291f61..e0dcf961 100644
--- a/src/components/WalletPass.svelte
+++ b/src/components/WalletPass.svelte
@@ -3,8 +3,24 @@
import { focus } from "$lib/utils";
import { t } from "$lib/translations";
+ import {
+ rememberForOptions,
+ defaultRememberForMs,
+ rememberWalletPassword,
+ forgetWalletPassword,
+ } from "$lib/passwordCache";
let { cancel, password = $bindable(), submit } = $props();
let revealPassword = $state();
+ let rememberForMs = $state(defaultRememberForMs);
+
+ let handleSubmit = async () => {
+ if (rememberForMs) {
+ rememberWalletPassword(password, rememberForMs);
+ } else {
+ forgetWalletPassword();
+ }
+ await submit();
+ };
</script>
<div
@@ -16,7 +32,7 @@
<h1 class="text-center text-2xl font-semibold">
{$t("payments.enterWalletPass")}
</h1>
- <form onsubmit={preventDefault(submit)}>
+ <form onsubmit={preventDefault(handleSubmit)}>
<div class="relative mb-5">
{#if revealPassword}
<input
@@ -49,6 +65,21 @@
></iconify-icon>
</button>
</div>
+ <div class="mb-5 space-y-2">
+ <label for="rememberFor" class="text-sm text-gray-500">
+ Remember for
+ </label>
+ <select
+ id="rememberFor"
+ class="w-full"
+ value={rememberForMs}
+ onchange={(e) => (rememberForMs = Number(e.target.value))}
+ >
+ {#each rememberForOptions as option}
+ <option value={option.ms}>{option.label}</option>
+ {/each}
+ </select>
+ </div>
<div class="w-full flex justify-center gap-2">
<button
type="button"
diff --git a/src/lib/passwordCache.ts b/src/lib/passwordCache.ts
new file mode 100644
index 00000000..4f764698
--- /dev/null
+++ b/src/lib/passwordCache.ts
@@ -0,0 +1,61 @@
+import { browser } from "$app/environment";
+import { writable } from "svelte/store";
+
+const PASSWORD_KEY = "walletPassword";
+const EXPIRES_KEY = "walletPasswordExpiresAt";
+
+export const rememberForOptions = [
+ { label: "Don't remember", ms: 0 },
+ { label: "8 hours", ms: 8 * 60 * 60 * 1000 },
+ { label: "1 day", ms: 24 * 60 * 60 * 1000 },
+ { label: "1 week", ms: 7 * 24 * 60 * 60 * 1000 },
+ { label: "1 month", ms: 30 * 24 * 60 * 60 * 1000 },
+ { label: "1 year", ms: 365 * 24 * 60 * 60 * 1000 },
+];
+
+export const defaultRememberForMs = 30 * 24 * 60 * 60 * 1000;
+
+const clearStoredPassword = () => {
+ if (!browser) return;
+ localStorage.removeItem(PASSWORD_KEY);
+ localStorage.removeItem(EXPIRES_KEY);
+};
+
+const loadStoredPassword = () => {
+ if (!browser) return undefined;
+ const password = localStorage.getItem(PASSWORD_KEY) || undefined;
+ if (!password) return undefined;
+ const expiresAtRaw = localStorage.getItem(EXPIRES_KEY);
+ const expiresAt = expiresAtRaw ? Number(expiresAtRaw) : undefined;
+ if (expiresAt && Date.now() > expiresAt) {
+ clearStoredPassword();
+ return undefined;
+ }
+ return password;
+};
+
+export const walletPassword = writable<string | undefined>(loadStoredPassword());
+
+export const rememberWalletPassword = (password: string, durationMs: number) => {
+ if (!browser) return;
+ if (!durationMs) {
+ clearStoredPassword();
+ walletPassword.set(undefined);
+ return;
+ }
+ const expiresAt = Date.now() + durationMs;
+ localStorage.setItem(PASSWORD_KEY, password);
+ localStorage.setItem(EXPIRES_KEY, String(expiresAt));
+ walletPassword.set(password);
+};
+
+export const forgetWalletPassword = () => {
+ clearStoredPassword();
+ walletPassword.set(undefined);
+};
+
+export const getRememberedWalletPassword = () => {
+ const password = loadStoredPassword();
+ walletPassword.set(password);
+ return password;
+};
diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte
index 3a399c3a..c56a487a 100644
--- a/src/routes/(app)/+layout.svelte
+++ b/src/routes/(app)/+layout.svelte
@@ -46,10 +46,30 @@
}
});
+ // Track last known Ark balance to compute incoming amount
+ let getLastArkBalance = () => {
+ return parseInt(localStorage.getItem("lastArkBalance") || "0");
+ };
+
+ let setLastArkBalance = (bal) => {
+ localStorage.setItem("lastArkBalance", bal.toString());
+ };
+
let handleArkPayment = async (notification) => {
console.log("ARK payment notification:", notification);
- let amount = notification.newVtxos.reduce((a, b) => a + b.value, 0);
- if (!amount) return;
+
+ // Calculate current total from newVtxos
+ let currentTotal = notification.newVtxos.reduce((a, b) => a + b.value, 0);
+ let lastBalance = getLastArkBalance();
+
+ // Compute delta (incoming amount)
+ let amount = currentTotal - lastBalance;
+
+ // Update tracked balance
+ setLastArkBalance(currentTotal);
+
+ // Only process positive incoming amounts
+ if (amount <= 0) return;
if ($fiat && $rateStore && user?.currency) {
success(`Received ${f(toFiat(amount, $rateStore), user.currency)}!`);
@@ -58,12 +78,12 @@
}
try {
- let inv = await post(`/invoice`, {
+ let inv = await post(`/post/invoice`, {
invoice: { type: "ark", amount },
user,
});
- await post(`/ark/receive`, {
+ await post(`/post/ark/receive`, {
amount,
iid: inv.id,
});
diff --git a/src/routes/(app)/account/[id]/+page.svelte b/src/routes/(app)/account/[id]/+page.svelte
index a8970ca7..db93a04d 100644
--- a/src/routes/(app)/account/[id]/+page.svelte
+++ b/src/routes/(app)/account/[id]/+page.svelte
@@ -8,6 +8,10 @@
import { t } from "$lib/translations";
import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english.js";
+ import {
+ getRememberedWalletPassword,
+ forgetWalletPassword,
+ } from "$lib/passwordCache";
let { data } = $props();
let { account, user } = data;
@@ -26,12 +30,36 @@
let cancel = $state(() => (passwordPrompt = false));
let toggle = () => (passwordPrompt = !passwordPrompt);
+ 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) {
+ forgetWalletPassword();
+ return false;
+ }
+ };
- let submit = $state(async () => {
+ let requestMnemonic = async () => {
+ if (await revealWithCachedPassword()) return;
toggle();
- const { decrypt } = await import("nostr-tools/nip49");
- let entropy = await decrypt(seed, password);
- mnemonic = entropyToMnemonic(entropy, wordlist);
+ };
+
+ let submit = $state(async () => {
+ passwordPrompt = false;
+ try {
+ const { decrypt } = await import("nostr-tools/nip49");
+ let entropy = await decrypt(seed, password);
+ mnemonic = entropyToMnemonic(entropy, wordlist);
+ } catch (e) {
+ fail("Invalid password, try again");
+ passwordPrompt = true;
+ }
});
let del = async () => {
@@ -71,7 +99,7 @@
<div class="my-auto">{$t("accounts.copy")}</div></button
>
{:else if seed}
- <button onclick={toggle} type="button" class="btn">
+ <button onclick={requestMnemonic} type="button" class="btn">
<iconify-icon noobserver icon="ph:eye-bold" width="32"
></iconify-icon>
<div class="my-auto">{$t("accounts.revealMnemonic")}</div></button
diff --git a/src/routes/(app)/account/ark/+page.svelte b/src/routes/(app)/account/ark/+page.svelte
index 99ab750b..aee80837 100644
--- a/src/routes/(app)/account/ark/+page.svelte
+++ b/src/routes/(app)/account/ark/+page.svelte
@@ -8,6 +8,12 @@
import { focus, fail, post, copy } from "$lib/utils";
import { goto } from "$app/navigation";
import { arkServerUrl } from "$lib/ark";
+ import {
+ rememberForOptions,
+ defaultRememberForMs,
+ rememberWalletPassword,
+ forgetWalletPassword,
+ } from "$lib/passwordCache";
let { data } = $props();
let { user } = data;
@@ -23,6 +29,7 @@
let confirm = $state("");
let revealPassword = $state(false);
let revealConfirm = $state(false);
+ let rememberForMs = $state(defaultRememberForMs);
let name = "Ark Vault";
let type = "ark";
@@ -73,6 +80,11 @@
let seed = await encrypt(hex.decode(privateKey), password);
await post("/account", { name, seed, type, arkAddress });
+ if (rememberForMs) {
+ rememberWalletPassword(password, rememberForMs);
+ } else {
+ forgetWalletPassword();
+ }
goto(`/${user.username}`);
} catch (e) {
console.log(e);
@@ -205,6 +217,22 @@
</button>
</label>
+ <div class="space-y-2">
+ <label for="rememberFor" class="text-sm text-secondary">
+ Remember for
+ </label>
+ <select
+ id="rememberFor"
+ class="w-full"
+ value={rememberForMs}
+ onchange={(e) => (rememberForMs = Number(e.target.value))}
+ >
+ {#each rememberForOptions as option}
+ <option value={option.ms}>{option.label}</option>
+ {/each}
+ </select>
+ </div>
+
<div class="flex gap-2">
<button
type="button"
diff --git a/src/routes/(app)/account/pass/+page.svelte b/src/routes/(app)/account/pass/+page.svelte
index 7c57679a..74e45f17 100644
--- a/src/routes/(app)/account/pass/+page.svelte
+++ b/src/routes/(app)/account/pass/+page.svelte
@@ -14,6 +14,12 @@
import { focus, versions, fail, post } from "$lib/utils";
import { goto } from "$app/navigation";
import { HDKey } from "@scure/bip32";
+ import {
+ rememberForOptions,
+ defaultRememberForMs,
+ rememberWalletPassword,
+ forgetWalletPassword,
+ } from "$lib/passwordCache";
let { data } = $props();
@@ -24,6 +30,7 @@
password = $state(),
revealPassword = $state(),
revealConfirm = $state();
+ let rememberForMs = $state(defaultRememberForMs);
let type = "bitcoin";
let name = $t("accounts.savings");
@@ -55,6 +62,11 @@
let pubkey = child.publicExtendedKey;
let fingerprint = child.fingerprint.toString(16).padStart(8, "0");
await post("/account", { fingerprint, pubkey, name, seed, type });
+ if (rememberForMs) {
+ rememberWalletPassword(password, rememberForMs);
+ } else {
+ forgetWalletPassword();
+ }
$mnemonic = "";
goto(`/${user.username}`);
} catch (e) {
@@ -159,6 +171,22 @@
</button>
</label>
+ <div class="space-y-2">
+ <label for="rememberFor" class="text-sm text-secondary">
+ Remember for
+ </label>
+ <select
+ id="rememberFor"
+ class="w-full"
+ value={rememberForMs}
+ onchange={(e) => (rememberForMs = Number(e.target.value))}
+ >
+ {#each rememberForOptions as option}
+ <option value={option.ms}>{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">
diff --git a/src/routes/(app)/invoice/[id]/tip/+page.svelte b/src/routes/(app)/invoice/[id]/tip/+page.svelte
index 41cc4636..4be2f536 100644
--- a/src/routes/(app)/invoice/[id]/tip/+page.svelte
+++ b/src/routes/(app)/invoice/[id]/tip/+page.svelte
@@ -1,7 +1,4 @@
<script>
- import { untrack } from "svelte";
-
- import { browser } from "$app/environment";
import { fiat, invoice as inv, request } from "$lib/store";
import { loc, copy, focus, f, sat, get, s, sats } from "$lib/utils";
import Slider from "$comp/Slider.svelte";
@@ -78,7 +75,7 @@
let locale = $derived(loc(user));
let qr;
- let tipPercent = $state();
+ let tipPercent = $state(amount > 0 && tip > 0 ? (tip / amount) * 100 : 0);
let fullscreen;
@@ -86,8 +83,7 @@
let rate = invoice.rate * (data.rate / data.invoiceRate);
$effect(() => {
- if (typeof tipPercent === "undefined") tipPercent = (tip / amount) * 100;
- tip = Math.round((amount / 100) * untrack(() => tipPercent));
+ tip = Math.round((amount / 100) * tipPercent);
});
let amountFiat = $derived(parseFloat(((amount * rate) / sats).toFixed(2)));
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 609e795b..c31ce60d 100644
--- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte
+++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte
@@ -17,6 +17,10 @@
import { HDKey } from "@scure/bip32";
import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english.js";
+ import {
+ getRememberedWalletPassword,
+ forgetWalletPassword,
+ } from "$lib/passwordCache";
import Amount from "$comp/Amount.svelte";
@@ -28,9 +32,29 @@
let cancel = $state(() => (passwordPrompt = false));
let signed = $state();
- let togglePassword = () => (passwordPrompt = !passwordPrompt);
+ let trySignWithCachedPassword = async () => {
+ const cached = getRememberedWalletPassword();
+ if (!cached) return false;
+ try {
+ password = cached;
+ await signTx();
+ return true;
+ } catch (e) {
+ forgetWalletPassword();
+ return false;
+ }
+ };
+
+ let togglePassword = async () => {
+ if (passwordPrompt) {
+ passwordPrompt = false;
+ return;
+ }
+ if (await trySignWithCachedPassword()) return;
+ passwordPrompt = true;
+ };
let handler = ({ cancel }) => {
- if (account.seed && !signed) cancel(), togglePassword();
+ if (account.seed && !signed) cancel(), void togglePassword();
return async ({ result }) => {
if (result.type === "redirect") {
goto(result.location);