aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-11-26 13:08:08 -0800
committerAdam Soltys <[email protected]>2024-11-26 13:20:53 -0800
commit9b3e117eafbba224b86dd7f8b3c56a0370f72d5b (patch)
treea670c3ae954d5075c851126c2aca7a1a45d1b41d /src
parent2761363d694d3f24600abbe656769f64d7a67605 (diff)
downloadcoinos-ui-9b3e117eafbba224b86dd7f8b3c56a0370f72d5b.tar.gz
coinos-ui-9b3e117eafbba224b86dd7f8b3c56a0370f72d5b.zip
bip21 and socket fixes
Diffstat (limited to 'src')
-rw-r--r--src/components/Account.svelte24
-rw-r--r--src/components/InvoiceActions.svelte2
-rw-r--r--src/components/InvoiceData.svelte2
-rw-r--r--src/lib/bip21.ts48
-rw-r--r--src/lib/invoice.ts1
-rw-r--r--src/lib/parse.ts32
-rw-r--r--src/lib/socket.ts24
-rw-r--r--src/lib/utils.ts4
-rw-r--r--src/routes/(app)/+layout.server.ts11
-rw-r--r--src/routes/(app)/+layout.svelte12
-rw-r--r--src/routes/(app)/[username]/+layout.server.ts2
-rw-r--r--src/routes/(app)/[username]/receive/+page.svelte20
-rw-r--r--src/routes/(app)/invoice/[id]/[email protected]110
-rw-r--r--src/routes/(app)/invoice/[id]/+page.svelte22
-rw-r--r--src/routes/(app)/payments/[...page]/+page.server.ts2
-rw-r--r--src/routes/(app)/payments/[...page]/+page.svelte186
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/+layout.server.ts18
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts5
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte35
-rw-r--r--src/routes/(app)/send/invoice/[id]/+page.svelte2
20 files changed, 249 insertions, 313 deletions
diff --git a/src/components/Account.svelte b/src/components/Account.svelte
index c378b9a3..4db77066 100644
--- a/src/components/Account.svelte
+++ b/src/components/Account.svelte
@@ -1,20 +1,13 @@
<script>
- import { run } from 'svelte/legacy';
+ import { run } from "svelte/legacy";
import { goto } from "$app/navigation";
import Icon from "$comp/Icon.svelte";
import Balance from "$comp/Balance.svelte";
import { t } from "$lib/translations";
- let {
- user,
- rate,
- account,
- last
- } = $props();
- let { name, seed, balance, id } = $state(account);
-
- let refresh = (a) => ({ seed, balance, id } = a);
+ let { user, rate, account, last } = $props();
+ let { name, seed, balance, id } = $derived(account);
let setAccount = () => (document.cookie = `aid=${id}; path=/; max-age=86400`);
let go = () => {
@@ -22,9 +15,6 @@
goto("/payments");
};
let type = $derived(seed ? "Savings" : "Cash");
- run(() => {
- refresh(account);
- });
</script>
<div
@@ -47,16 +37,18 @@
</div>
<div class="flex justify-center w-full text-xl gap-2">
- <a href={`/invoice`} class="contents" onclick={setAccount}>
+ <a href={"/invoice"} class="contents" onclick={setAccount}>
<button class="btn !w-auto flex-grow">
- <iconify-icon icon="ph:hand-coins-bold" width="32" flip="horizontal"></iconify-icon>
+ <iconify-icon icon="ph:hand-coins-bold" width="32" flip="horizontal"
+ ></iconify-icon>
<div class="my-auto">{$t("user.dashboard.receive")}</div>
</button>
</a>
<a href={`/send`} class="contents grow" onclick={setAccount}>
<button type="button" class="btn !w-auto flex-grow">
- <iconify-icon icon="ph:paper-plane-right-bold" width="32"></iconify-icon>
+ <iconify-icon icon="ph:paper-plane-right-bold" width="32"
+ ></iconify-icon>
<div class="my-auto">{$t("user.dashboard.send")}</div>
</button>
</a>
diff --git a/src/components/InvoiceActions.svelte b/src/components/InvoiceActions.svelte
index 0b5dca55..cd66fc36 100644
--- a/src/components/InvoiceActions.svelte
+++ b/src/components/InvoiceActions.svelte
@@ -35,7 +35,7 @@
</a>
{/if}
- {#if txt.startsWith("ln")}
+ {#if txt.length > 120}
<div class="w-full flex justify-center gap-2 flex-wrap">
<button class="btn" onclick={() => (showQr = !showQr)}>
{#if showQr}
diff --git a/src/components/InvoiceData.svelte b/src/components/InvoiceData.svelte
index c6721829..b511c599 100644
--- a/src/components/InvoiceData.svelte
+++ b/src/components/InvoiceData.svelte
@@ -26,7 +26,7 @@
</div>
{/if}
-{#if !showQr || !txt.startsWith("ln")}
+{#if !showQr || txt.length <= 120}
<div class="break-all text-center text-secondary text-xl">
{txt}
</div>
diff --git a/src/lib/bip21.ts b/src/lib/bip21.ts
new file mode 100644
index 00000000..2642afab
--- /dev/null
+++ b/src/lib/bip21.ts
@@ -0,0 +1,48 @@
+export function decode(uri, allowedSchemes = ["bitcoin", "liquidnetwork"]) {
+ // Determine the scheme from the URI
+ const split = uri.indexOf(":");
+ if (split === -1) {
+ throw new Error("Invalid URI: Missing scheme");
+ }
+
+ const urnScheme = uri.slice(0, split).toLowerCase();
+ if (!allowedSchemes.includes(urnScheme)) {
+ throw new Error(`Invalid scheme: ${urnScheme}`);
+ }
+
+ // Extract address and query string
+ const queryStart = uri.indexOf("?");
+ const address = uri.slice(
+ split + 1,
+ queryStart === -1 ? undefined : queryStart,
+ );
+ const query = queryStart === -1 ? "" : uri.slice(queryStart + 1);
+ const options = Object.fromEntries(new URLSearchParams(query));
+
+ const n = Number(options?.amount);
+ if (!Number.isFinite(n)) throw new Error("Invalid amount");
+ if (n < 0) throw new Error("Invalid amount");
+
+ return { scheme: urnScheme, address, options };
+}
+
+export function encode(
+ address,
+ options = { amount: "" },
+ urnScheme = "bitcoin",
+) {
+ if (!address || typeof address !== "string") {
+ throw new TypeError("Invalid address");
+ }
+
+ if (!["bitcoin", "liquidnetwork"].includes(urnScheme)) {
+ throw new Error(`Unsupported scheme: ${urnScheme}`);
+ }
+
+ const n = Number(options?.amount);
+ if (!Number.isFinite(n)) throw new Error("Invalid amount");
+ if (n < 0) throw new Error("Invalid amount");
+
+ const query = new URLSearchParams(options).toString();
+ return `${urnScheme}:${address}${query ? `?${query}` : ""}`;
+}
diff --git a/src/lib/invoice.ts b/src/lib/invoice.ts
index 9563d924..a48e02a4 100644
--- a/src/lib/invoice.ts
+++ b/src/lib/invoice.ts
@@ -2,7 +2,6 @@ import { auth, get, post } from "$lib/utils";
import { redirect } from "@sveltejs/kit";
export default async ({ cookies, request }) => {
- console.log("LOADING");
const form = await request.formData();
const aid = form.get("aid") || cookies.get("aid");
diff --git a/src/lib/parse.ts b/src/lib/parse.ts
index 6483aaa0..7ddb2104 100644
--- a/src/lib/parse.ts
+++ b/src/lib/parse.ts
@@ -1,7 +1,7 @@
import { PUBLIC_DOMAIN } from "$env/static/public";
+import { decode } from "$lib/bip21";
import { get, isLiquid, post, sats } from "$lib/utils";
import { redirect } from "@sveltejs/kit";
-import bip21 from "bip21";
import { validate } from "bitcoin-address-validation";
export default async (s, host) => {
@@ -21,6 +21,23 @@ export default async (s, host) => {
} catch (e) {}
}
+ // bitcoin
+ if (
+ t.toLowerCase().startsWith("bitcoin:") ||
+ t.toLowerCase().startsWith("liquidnetwork:")
+ ) {
+ ({
+ address: t,
+ options: { amount },
+ } = decode(t));
+ }
+
+ if (validate(t) || isLiquid(t)) {
+ let r = `/send/bitcoin/${t}`;
+ if (amount) r += `/${Math.round(amount * sats)}`;
+ redirect(307, r);
+ }
+
// lightning
if (t.toLowerCase().startsWith("lightning:"))
t = t.toLowerCase().replace("lightning:", "");
@@ -47,19 +64,6 @@ export default async (s, host) => {
}
}
- // bitcoin
- if (t.toLowerCase().startsWith("bitcoin:"))
- ({
- address: t,
- options: { amount },
- } = bip21.decode(t));
-
- if (validate(t) || isLiquid(t)) {
- let r = `/send/bitcoin/${t}`;
- if (amount) r += `/${Math.round(amount * sats)}`;
- redirect(307, r);
- }
-
if (t.startsWith("cashu")) {
const { id } = await post("/cash", { token: t });
redirect(307, `/ecash/${id}`);
diff --git a/src/lib/socket.ts b/src/lib/socket.ts
index 566e989d..bd8494fa 100644
--- a/src/lib/socket.ts
+++ b/src/lib/socket.ts
@@ -3,9 +3,10 @@ import { navigating, page } from "$app/stores";
import { PUBLIC_SOCKET } from "$env/static/public";
import { event, invoice, last, request } from "$lib/store";
import { s, sleep, success, wait } from "$lib/utils";
+import cookies from "js-cookie";
import { get } from "svelte/store";
-let socket;
+export let socket;
let token;
export const auth = () => token && send("login", token);
@@ -13,8 +14,9 @@ export const auth = () => token && send("login", token);
export const send = async (type, data) => {
try {
await wait(() => socket && socket.readyState === 1, 1000, 10);
- } catch (e: any) {
- if (e.message === "timeout") reconnectToWebsocket();
+ } catch (e) {
+ const { message } = e as Error;
+ if (message === "timeout") reconnectToWebsocket();
}
await wait(() => socket.readyState === 1, 1000, 10);
@@ -47,8 +49,14 @@ export const messages = (data) => ({
url: { pathname },
} = get(page);
+ const username = cookies.get("username");
+
if (amount > 0) {
- if (pathname.includes("receive")) {
+ if (
+ pathname.includes(`/${username}`) ||
+ pathname.includes("/receive") ||
+ pathname.includes("/invoice")
+ ) {
await wait(() => !get(navigating));
await goto(`/invoice/${iid}`);
} else success(`${confirmed ? "Received" : "Detected"} ⚡️${s(amount)}!`);
@@ -63,7 +71,14 @@ const maxReconnectDelay = 16000;
let currentReconnectDelay = initialReconnectDelay;
+let connecting;
export function connect(t) {
+ if (connecting) return;
+ connecting = true;
+ setTimeout(() => {
+ connecting = false;
+ }, 5000);
+
token = t;
if (socket) return auth();
@@ -87,6 +102,7 @@ async function onWebsocketMessage(msg) {
function onWebsocketOpen() {
currentReconnectDelay = initialReconnectDelay;
+ send("heartbeat", token);
}
function onWebsocketClose() {
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index a15bb8e7..dc7182e3 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -151,7 +151,7 @@ export const info = (m: string) => {
export const login = async (
user: { username: string; password: string },
- cookies: any,
+ cookies,
ip: string,
) => {
const maxAge = 380 * 24 * 60 * 60;
@@ -182,7 +182,7 @@ export const login = async (
cookies.set("token", token, opts);
};
-export const auth = (cookies: any) => ({
+export const auth = (cookies) => ({
authorization: `Bearer ${cookies.get("token")}`,
});
diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts
index 3c5b3b3e..d490a684 100644
--- a/src/routes/(app)/+layout.server.ts
+++ b/src/routes/(app)/+layout.server.ts
@@ -3,7 +3,7 @@ import { error, redirect } from "@sveltejs/kit";
export async function load({ cookies, request, url, params }) {
const { pathname } = url;
- const { username } = params;
+ const { id, username } = params;
const token = cookies.get("token");
let rate;
let rates = { USD: 1 };
@@ -20,7 +20,14 @@ export async function load({ cookies, request, url, params }) {
}
let subject;
- if (username) {
+
+ if (url.pathname.startsWith("/invoice") && id) {
+ try {
+ ({ user: subject } = await get(`/invoice/${id}`));
+ } catch (e) {
+ console.log("unable to fetch invoice", id, e);
+ }
+ } else if (username) {
try {
subject = await get(`/users/${username}`);
} catch (e) {
diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte
index aaaf543f..49e4bdb9 100644
--- a/src/routes/(app)/+layout.svelte
+++ b/src/routes/(app)/+layout.svelte
@@ -1,7 +1,7 @@
<script>
import { SvelteToast } from "@zerodevx/svelte-toast";
import { onDestroy, onMount } from "svelte";
- import { close, connect, send } from "$lib/socket";
+ import { close, connect, send, socket } from "$lib/socket";
import {
last,
invoice,
@@ -16,7 +16,6 @@
import { browser } from "$app/environment";
import LoadingSplash from "$comp/LoadingSplash.svelte";
import AppHeader from "$comp/AppHeader.svelte";
- import Invoice from "$comp/Invoice.svelte";
import Password from "$comp/Password.svelte";
import { getCookie, warning, protectedRoutes } from "$lib/utils";
import { t, locale, loading } from "$lib/translations";
@@ -29,7 +28,6 @@
$themeStore = theme;
$effect(() => (theme = $themeStore));
- $effect(() => browser && connect(token));
afterNavigate(() => {
if (user) {
@@ -64,14 +62,13 @@
}
});
- let lost,
- checkTimer,
- expireTimer,
+ let checkTimer,
counter = 0;
let checkSocket = () => {
counter++;
- lost = Date.now() - $last > 30000;
+ let lost = socket?.readyState !== 1 || !$last || Date.now() - $last > 30000;
+ console.log("LOST?", lost);
if (lost) connect(token);
if (counter > 5) {
send("heartbeat", token);
@@ -85,7 +82,6 @@
if (browser) {
close();
clearTimeout(checkTimer);
- clearTimeout(expireTimer);
}
});
</script>
diff --git a/src/routes/(app)/[username]/+layout.server.ts b/src/routes/(app)/[username]/+layout.server.ts
index 1dd163e9..bff8848c 100644
--- a/src/routes/(app)/[username]/+layout.server.ts
+++ b/src/routes/(app)/[username]/+layout.server.ts
@@ -15,7 +15,7 @@ export const load = async ({
const expires = new Date();
expires.setSeconds(expires.getSeconds() + 380 * 24 * 60 * 60);
- cookies.set("username", username, { path: "/", expires });
+ cookies.set("username", username, { path: "/", expires, httpOnly: false });
return { subject, user };
} catch (e) {
diff --git a/src/routes/(app)/[username]/receive/+page.svelte b/src/routes/(app)/[username]/receive/+page.svelte
index ddb17e71..89f3a1f3 100644
--- a/src/routes/(app)/[username]/receive/+page.svelte
+++ b/src/routes/(app)/[username]/receive/+page.svelte
@@ -38,16 +38,18 @@
tip;
let setType = async (type) => {
- invoice.type = type;
- ({ id } = await post(`/invoice`, {
- invoice,
- user: { username, currency },
- }));
+ $showQr = true;
+ if (type === types.lightning && !amount)
+ goto(`/${username}/receive`, { invalidateAll: true, noScroll: true });
+ else {
+ invoice.type = type;
+ ({ id } = await post(`/invoice`, {
+ invoice,
+ user: { username, currency },
+ }));
- goto(`/invoice/${id}?options=true`, {
- invalidateAll: true,
- noScroll: true,
- });
+ goto(`/invoice/${id}?options=true`, { invalidateAll: true, noScroll: true });
+ }
};
let setAmount = async (e) => {
diff --git a/src/routes/(app)/invoice/[id]/[email protected] b/src/routes/(app)/invoice/[id]/[email protected]
deleted file mode 100644
index 13b121d8..00000000
--- a/src/routes/(app)/invoice/[id]/[email protected]
+++ /dev/null
@@ -1,110 +0,0 @@
-<script>
- import { SvelteToast } from "@zerodevx/svelte-toast";
- import { onDestroy, onMount } from "svelte";
- import { close, connect, send } from "$lib/socket";
- import {
- last,
- invoice,
- request,
- ndef,
- passwordPrompt,
- password,
- pin,
- theme as themeStore,
- } from "$lib/store";
- import { page } from "$app/stores";
- import { browser } from "$app/environment";
- import LoadingSplash from "$comp/LoadingSplash.svelte";
- import AppHeader from "$comp/AppHeader.svelte";
- import Invoice from "$comp/Invoice.svelte";
- import Password from "$comp/Password.svelte";
- import { getCookie, warning, protectedRoutes } from "$lib/utils";
- import { t, locale, loading } from "$lib/translations";
- import { goto, onNavigate } from "$app/navigation";
-
- let { data, children } = $props();
-
- let { user, subject, token } = $derived(data);
- let { theme } = $state(data);
-
- $themeStore = theme;
- $effect(() => (theme = $themeStore));
- $effect(() => browser && connect(token));
-
- onMount(async () => {
- if (browser) {
- checkSocket();
- $pin = getCookie("pin");
-
- if (window.NDEFReader && user.nfc) {
- try {
- $ndef = new NDEFReader();
- await $ndef.scan();
-
- $ndef.addEventListener("readingerror", (e) => {
- console.log("nfc error", e);
- });
-
- $ndef.addEventListener("reading", ({ message, serialNumber }) => {
- goto(`/send/${message}`);
- });
- } catch (error) {
- console.log("Argh! " + error);
- }
- }
- }
- });
-
- let lost,
- checkTimer,
- expireTimer,
- counter = 0;
-
- let checkSocket = () => {
- counter++;
- lost = Date.now() - $last > 30000;
- if (lost) connect(token);
- if (counter > 5) {
- send("heartbeat", token);
- counter = 0;
- }
-
- checkTimer = setTimeout(checkSocket, 1000);
- };
-
- onDestroy(() => {
- if (browser) {
- close();
- clearTimeout(checkTimer);
- clearTimeout(expireTimer);
- }
- });
-</script>
-
-{#if browser && user && $passwordPrompt}
- <Password {user} />
-{/if}
-
-<svelte:head>
- {#if subject}
- <title>coinos - {subject.username}</title>
- <meta name="lightning" content={`lnurlp:${subject.username}@coinos.io`} />
- {:else}
- <title>coinos</title>
- {/if}
-
- {#if subject?.profile}
- <meta name="og:image" content={`/api/public/${subject.profile}.webp`} />
- {:else}
- <meta property="og:image" content="/images/logo.webp" />
- {/if}
-</svelte:head>
-
-<SvelteToast options={{ reversed: true, intro: { y: 192 } }} />
-
-<main class="pb-4 min-h-dvh" data-theme={theme}>
- <AppHeader {user} {subject} />
- {#if !$loading}
- {@render children?.()}
- {/if}
-</main>
diff --git a/src/routes/(app)/invoice/[id]/+page.svelte b/src/routes/(app)/invoice/[id]/+page.svelte
index 8c6e2b75..3ad20f68 100644
--- a/src/routes/(app)/invoice/[id]/+page.svelte
+++ b/src/routes/(app)/invoice/[id]/+page.svelte
@@ -56,13 +56,21 @@
});
let setType = async (type) => {
- invoice.type = type;
- ({ id } = await post(`/invoice`, {
- invoice,
- user: { username, currency },
- }));
+ $showQr = true;
+ if (type === types.lightning && !amount)
+ goto(`/${username}/receive`, { invalidateAll: true, noScroll: true });
+ else {
+ invoice.type = type;
+ ({ id } = await post(`/invoice`, {
+ invoice,
+ user: { username, currency },
+ }));
- goto(`./${id}?options=true`, { invalidateAll: true, noScroll: true });
+ goto(`/invoice/${id}?options=true`, {
+ invalidateAll: true,
+ noScroll: true,
+ });
+ }
};
let setAmount = async (e) => {
@@ -74,7 +82,7 @@
if (typeof $amountPrompt === "undefined") $amountPrompt = true;
invoice.amount = newAmount;
invoice.tip = 0;
-
+
({ id } = await post(`/invoice`, {
invoice,
user: { username, currency },
diff --git a/src/routes/(app)/payments/[...page]/+page.server.ts b/src/routes/(app)/payments/[...page]/+page.server.ts
index 268fd32b..05ed2d64 100644
--- a/src/routes/(app)/payments/[...page]/+page.server.ts
+++ b/src/routes/(app)/payments/[...page]/+page.server.ts
@@ -1,4 +1,4 @@
-import { get, auth } from "$lib/utils";
+import { auth, get } from "$lib/utils";
const limit = 25;
diff --git a/src/routes/(app)/payments/[...page]/+page.svelte b/src/routes/(app)/payments/[...page]/+page.svelte
index cda7c350..146a286d 100644
--- a/src/routes/(app)/payments/[...page]/+page.svelte
+++ b/src/routes/(app)/payments/[...page]/+page.svelte
@@ -11,8 +11,9 @@
import { goto, invalidate } from "$app/navigation";
import locales from "$lib/locales";
let { data } = $props();
+ let { start, end, user, rates, payments, incoming, outgoing } =
+ $derived(data);
- let { start, end, user, rates, payments } = $state(data);
let locale = locales[user.language];
let change = ({ target: { value } }) => goto(value);
@@ -55,19 +56,13 @@
);
let p = $state(),
- incoming = $state(),
- outgoing = $state(),
pages = $state([]);
- run(() => {
- data &&
- ({ page: p, pages, start, end, incoming, outgoing, payments } = data);
- });
- run(() => {
- $page && ($newPayment = false);
- });
- run(() => {
- $newPayment && invalidate(`/users/${user.username}`);
+ $effect(() => {
+ if ($newPayment) {
+ $newPayment = false;
+ invalidate(`/users/${user.username}`);
+ }
});
let path = $derived(
@@ -204,104 +199,101 @@
<div class="text-base">
{#each payments as p, i}
- <div
- class="grid grid-cols-12 border-b border-base-200 hover:bg-base-200 px-1 py-2 lg:p-4 cursor-pointer"
- class:border-b-0={i === payments.length - 1}
- class:text-error={p.amount < 0}
- onclick={() => goto(`/payment/${p.id}`)}
- >
- <div class="whitespace-nowrap my-auto col-span-3">
- <div
- class="font-bold flex items-center"
- >
- <div class="flex items-center">
- <iconify-icon
- icon="ph:lightning-fill"
- width="24"
- class="text-yellow-300"
- ></iconify-icon>
- <div>{s(Math.abs(p.amount))}</div>
- </div>
+ <div
+ class="grid grid-cols-12 border-b border-base-200 hover:bg-base-200 px-1 py-2 lg:p-4 cursor-pointer"
+ class:border-b-0={i === payments.length - 1}
+ class:text-error={p.amount < 0}
+ onclick={() => goto(`/payment/${p.id}`)}
+ >
+ <div class="whitespace-nowrap my-auto col-span-3">
+ <div class="font-bold flex items-center">
+ <div class="flex items-center">
+ <iconify-icon
+ icon="ph:lightning-fill"
+ width="24"
+ class="text-yellow-300"
+ ></iconify-icon>
+ <div>{s(Math.abs(p.amount))}</div>
</div>
+ </div>
- <div class="text-secondary flex items-center text-base">
- {f(Math.abs(p.amount) * (p.rate / sats), p.currency)}
+ <div class="text-secondary flex items-center text-base">
+ {f(Math.abs(p.amount) * (p.rate / sats), p.currency)}
- {#if p.tip}
- <span class="text-sm text-secondary">
- &nbsp;+{Math.round((p.tip / Math.abs(p.amount)) * 100)}%
- </span>
- {/if}
- </div>
+ {#if p.tip}
+ <span class="text-sm text-secondary">
+ &nbsp;+{Math.round((p.tip / Math.abs(p.amount)) * 100)}%
+ </span>
+ {/if}
</div>
+ </div>
- <div
- class="flex my-auto col-span-6 truncate text-ellipsis overflow-hidden mx-auto"
- >
- {#if p.type === types.pot}
- <a href={`/pot/${p.memo}`}>
- <div class="text-secondary flex">
- <div class="my-auto mr-1">
- <img src="/images/moneypot.png" class="w-12" alt="Pot" />
- </div>
-
- <div class="my-auto">Pot</div>
- </div>
- </a>
- {:else if p.with}
- <div class="flex">
- <div class="my-auto">
- <Avatar user={p.with} size={12} disabled={true} />
- </div>
- <div class="my-auto ml-1 text-secondary">
- {p.with.username}
+ <div
+ class="flex my-auto col-span-6 truncate text-ellipsis overflow-hidden mx-auto"
+ >
+ {#if p.type === types.pot}
+ <a href={`/pot/${p.memo}`}>
+ <div class="text-secondary flex">
+ <div class="my-auto mr-1">
+ <img src="/images/moneypot.png" class="w-12" alt="Pot" />
</div>
- </div>
- {:else}
- <div class="text-secondary flex items-center gap-1">
- {#if p.type === types.lightning}
- <iconify-icon
- icon="ph:lightning-fill"
- class="text-yellow-300 text-3xl"
- ></iconify-icon>
- {:else if p.type === types.ecash}
- <img src="/images/cash.png" class="w-12" />
- {:else if p.type === types.reconcile}
- <iconify-icon icon="ph:scales-bold" width="32"
- ></iconify-icon>
- {:else if p.type === types.bitcoin}
- <iconify-icon icon="logos:bitcoin" class="text-3xl"
- ></iconify-icon>
- {:else if p.type === types.liquid}
- <div class="my-auto">
- <img
- src="/images/liquid.svg"
- class="w-10 border-4 border-transparent"
- alt="Liquid"
- />
- </div>
- {/if}
+ <div class="my-auto">Pot</div>
+ </div>
+ </a>
+ {:else if p.with}
+ <div class="flex">
+ <div class="my-auto">
+ <Avatar user={p.with} size={12} disabled={true} />
+ </div>
+ <div class="my-auto ml-1 text-secondary">
+ {p.with.username}
+ </div>
+ </div>
+ {:else}
+ <div class="text-secondary flex items-center gap-1">
+ {#if p.type === types.lightning}
+ <iconify-icon
+ icon="ph:lightning-fill"
+ class="text-yellow-300 text-3xl"
+ ></iconify-icon>
+ {:else if p.type === types.ecash}
+ <img src="/images/cash.png" class="w-12" />
+ {:else if p.type === types.reconcile}
+ <iconify-icon icon="ph:scales-bold" width="32"></iconify-icon>
+ {:else if p.type === types.bitcoin}
+ <iconify-icon icon="logos:bitcoin" class="text-3xl"
+ ></iconify-icon>
+ {:else if p.type === types.liquid}
<div class="my-auto">
- {p.amount > 0
- ? p.confirmed
- ? "Received"
- : "Pending"
- : "Sent"}
+ <img
+ src="/images/liquid.svg"
+ class="w-10 border-4 border-transparent"
+ alt="Liquid"
+ />
</div>
- </div>
- {/if}
- </div>
+ {/if}
- <div class="text-secondary text-right text-sm my-auto col-span-3">
- <div>
- {format(new Date(p.created), "h:mm aaa", { locale })}
- </div>
- <div>
- {format(new Date(p.created), "MMM d, yy", { locale })}
+ <div class="my-auto">
+ {p.amount > 0
+ ? p.confirmed
+ ? "Received"
+ : "Pending"
+ : "Sent"}
+ </div>
</div>
+ {/if}
+ </div>
+
+ <div class="text-secondary text-right text-sm my-auto col-span-3">
+ <div>
+ {format(new Date(p.created), "h:mm aaa", { locale })}
+ </div>
+ <div>
+ {format(new Date(p.created), "MMM d, yy", { locale })}
</div>
</div>
+ </div>
{:else}
<p class="text-secondary text-lg text-center">{$t("payments.empty")}</p>
{/each}
diff --git a/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts b/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts
index 68191311..a321ec43 100644
--- a/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts
+++ b/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts
@@ -4,20 +4,22 @@ import { redirect } from "@sveltejs/kit";
export async function load({ cookies, params, parent }) {
const { user } = await parent();
const aid = cookies.get("aid");
- const { address } = params;
+ const { address, amount } = params;
const { balance } = await get(`/account/${aid}`, auth(cookies));
+ let invoice;
try {
- const invoice = await get(`/invoice/${address}`);
-
- if (invoice) {
- const recipient = await get(`/users/${invoice.uid}`);
- if (recipient?.id !== user.id)
- redirect(307, `/pay/${recipient.username}`);
- }
+ invoice = await get(`/invoice/${address}`);
} catch (e) {
// invoice not found
}
+ if (invoice) {
+ const recipient = await get(`/users/${invoice.uid}`);
+ let r = `/pay/${recipient.username}`;
+ if (amount) r += `/${amount}`;
+ if (recipient?.id !== user.id) redirect(307, r);
+ }
+
return { balance };
}
diff --git a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts
index 1122e689..9156bf7b 100644
--- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts
+++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts
@@ -39,9 +39,10 @@ export const actions = {
try {
const body = await fd(request);
p = await post("/bitcoin/send", body, auth(cookies));
- } catch (e: any) {
+ } catch (e) {
console.log("problem sending bitcoin", e);
- return fail(400, { address, amount, feeRate, message: e.message });
+ const { message } = e as Error;
+ return fail(400, { address, amount, feeRate, message });
}
redirect(307, `/sent/${p.id}`);
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 f37d4479..298280df 100644
--- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte
+++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte
@@ -5,19 +5,10 @@
import { t } from "$lib/translations";
import { enhance } from "$app/forms";
import Toggle from "$comp/Toggle.svelte";
- import Icon from "$comp/Icon.svelte";
import Spinner from "$comp/Spinner.svelte";
import WalletPass from "$comp/WalletPass.svelte";
import { page } from "$app/stores";
- import {
- toFiat,
- f,
- focus,
- s,
- sat,
- closest,
- network,
- } from "$lib/utils";
+ import { toFiat, f, focus, s, sat, closest, network } from "$lib/utils";
import { pin } from "$lib/store";
import { goto, invalidate } from "$app/navigation";
import { rate } from "$lib/store";
@@ -30,6 +21,8 @@
import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english";
+ import Amount from "$comp/Amount.svelte";
+
let { data, form } = $props();
let { encode, decode } = hexUtil;
@@ -130,34 +123,20 @@
{:else}
<div class="text-xl text-secondary break-all">{address}</div>
- <div class="text-center">
- <h2 class="text-2xl md:text-3xl font-semibold">
- {f(toFiat(amount, $rate), currency)}
- </h2>
- <h3 class="text-secondary md:text-lg mb-6 mt-1">⚡️{s(amount)}</h3>
- </div>
+ <Amount {amount} rate={$rate} {currency} />
<div class="text-center">
<h2 class="text-secondary text-lg">{$t("payments.networkFee")}</h2>
<div class="flex flex-wrap gap-4 justify-center">
- <select
- bind:value={feeRate}
- onchange={setFee}
- class="border text-lg bg-white p-4 rounded-2xl text-center my-auto"
- >
+ <select bind:value={feeRate} onchange={setFee}>
{#each Object.keys(feeNames) as feeName}
<option value={fees[feeName]}
- >{feeNames[feeName]} &mdash; ⚡️{fees[feeName]}/vb</option
+ >{feeNames[feeName]} &mdash; {fees[feeName]} sats/vb</option
>
{/each}
</select>
- <div class="my-auto">
- <h2 class="text-xl">
- {f(toFiat(fee, $rate), currency)}
- </h2>
- <h3 class="text-secondary">⚡️{s(fee)}</h3>
- </div>
+ <Amount amount={fee} rate={$rate} {currency} />
</div>
</div>
diff --git a/src/routes/(app)/send/invoice/[id]/+page.svelte b/src/routes/(app)/send/invoice/[id]/+page.svelte
index f5259170..d2b3727c 100644
--- a/src/routes/(app)/send/invoice/[id]/+page.svelte
+++ b/src/routes/(app)/send/invoice/[id]/+page.svelte
@@ -103,7 +103,7 @@
<Amount {amount} {tip} {rate} {currency} />
- <h1 class="text-xl md:text-2xl text-secondary">
+ <h1 class="text-xl text-secondary">
{$t("payments.to")}
</h1>