aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-05 16:44:55 -0800
committerAdam Soltys <[email protected]>2026-02-11 15:34:32 -0800
commitcf1d8029efc1f9497bcef71b5c959c42794f3566 (patch)
treeb3a9658e7e426a82a545477df0414bc6b61509f2 /src
parented4d86167d32503ad8813c81dda9591d1b1b8190 (diff)
downloadcoinos-ui-cf1d8029efc1f9497bcef71b5c959c42794f3566.tar.gz
coinos-ui-cf1d8029efc1f9497bcef71b5c959c42794f3566.zip
payment forwarding
Diffstat (limited to 'src')
-rw-r--r--src/lib/ark.ts29
-rw-r--r--src/lib/parse.ts2
-rw-r--r--src/routes/(app)/+layout.svelte22
-rw-r--r--src/routes/(app)/invoice/[id]/+layout.server.ts3
-rw-r--r--src/routes/(app)/send/invoice/[id]/+page.server.ts1
-rw-r--r--src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts13
-rw-r--r--src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte52
7 files changed, 89 insertions, 33 deletions
diff --git a/src/lib/ark.ts b/src/lib/ark.ts
index 26c16a93..25122a8c 100644
--- a/src/lib/ark.ts
+++ b/src/lib/ark.ts
@@ -53,11 +53,38 @@ export const syncTransactions = async (aid: string) => {
return post("/post/ark/sync", { transactions, aid });
};
+export const vtxoKey = (v: { txid: string; vout: number }) =>
+ `${v.txid}:${v.vout}`;
+
+export const getProcessedVtxos = (): Set<string> => {
+ try {
+ return new Set(
+ JSON.parse(localStorage.getItem("processedArkVtxos") || "[]"),
+ );
+ } catch {
+ return new Set();
+ }
+};
+
+export const addProcessedVtxos = (keys: string[]) => {
+ const processed = getProcessedVtxos();
+ for (const k of keys) processed.add(k);
+ const arr = [...processed].slice(-200);
+ localStorage.setItem("processedArkVtxos", JSON.stringify(arr));
+};
+
export const sendArk = async (address: string, amount: number) => {
const wallet = await getWallet();
if (!wallet) throw new Error("Ark wallet not available");
- return wallet.sendBitcoin({ address, amount });
+ const txid = await wallet.sendBitcoin({ address, amount });
+
+ // Mark post-send VTXOs as processed so notifyIncomingFunds
+ // ignores change VTXOs from this send
+ const vtxos = await wallet.getVtxos({ spendableOnly: false });
+ if (vtxos?.length) addProcessedVtxos(vtxos.map(vtxoKey));
+
+ return txid;
};
export const settle = async () => {
diff --git a/src/lib/parse.ts b/src/lib/parse.ts
index 8520c27a..3249b9db 100644
--- a/src/lib/parse.ts
+++ b/src/lib/parse.ts
@@ -72,8 +72,10 @@ export default async (s, host, cookies) => {
invoice = { id: iid };
} else if (t.toLowerCase().startsWith("ln")) {
try {
+ const aid = cookies.get("aid");
invoice = await get(`/invoice/${t}`);
if (invoice.user.username === "mint") throw new Error("mint payment");
+ if (aid && aid !== invoice.aid) throw new Error("cross-account");
} catch (e) {
if (t.toLowerCase().startsWith("lno")) {
const { offer_amount_msat: a } = await get(`/decode/${t}`);
diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte
index d9f6a0d0..587a4b98 100644
--- a/src/routes/(app)/+layout.svelte
+++ b/src/routes/(app)/+layout.svelte
@@ -1,5 +1,5 @@
<script>
- import { getWallet, getAddress, getVtxos, syncTransactions, settle } from "$lib/ark";
+ import { getWallet, getAddress, getVtxos, syncTransactions, settle, vtxoKey, getProcessedVtxos, addProcessedVtxos } from "$lib/ark";
import { SvelteToast } from "@zerodevx/svelte-toast";
import { onDestroy, onMount } from "svelte";
import { close, connect, send, socket } from "$lib/socket";
@@ -46,26 +46,6 @@
}
});
- // Get unique VTXO key (txid:vout like arkade-wallet uses)
- let vtxoKey = (v) => `${v.txid}:${v.vout}`;
-
- // Track processed VTXO keys to avoid duplicates
- let getProcessedVtxos = () => {
- try {
- return new Set(JSON.parse(localStorage.getItem("processedArkVtxos") || "[]"));
- } catch {
- return new Set();
- }
- };
-
- let addProcessedVtxos = (keys) => {
- let processed = getProcessedVtxos();
- keys.forEach((k) => processed.add(k));
- // Keep only last 200 to avoid unbounded growth
- let arr = [...processed].slice(-200);
- localStorage.setItem("processedArkVtxos", JSON.stringify(arr));
- };
-
// Prevent concurrent processing of Ark notifications
let arkProcessing = false;
diff --git a/src/routes/(app)/invoice/[id]/+layout.server.ts b/src/routes/(app)/invoice/[id]/+layout.server.ts
index ab41dddc..ce76d150 100644
--- a/src/routes/(app)/invoice/[id]/+layout.server.ts
+++ b/src/routes/(app)/invoice/[id]/+layout.server.ts
@@ -24,9 +24,10 @@ export async function load({ cookies, depends, params, url, parent }) {
subject = invoice.user;
+ const aid = cookies.get("aid") || user?.id;
if (
user &&
- invoice.uid !== user?.id &&
+ (invoice.uid !== user?.id || aid !== invoice.aid) &&
!(url.pathname.includes("tip") || url.pathname.includes("memo")) &&
!options
) {
diff --git a/src/routes/(app)/send/invoice/[id]/+page.server.ts b/src/routes/(app)/send/invoice/[id]/+page.server.ts
index 70b007a8..c9c7dbd9 100644
--- a/src/routes/(app)/send/invoice/[id]/+page.server.ts
+++ b/src/routes/(app)/send/invoice/[id]/+page.server.ts
@@ -64,6 +64,7 @@ export const actions = {
try {
const body = await fd(request);
body.hash = id;
+ body.aid = cookies.get("aid");
p = await post("/payments", body, auth(cookies));
} catch (e) {
diff --git a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts
index 5c3e297c..e6cd0e76 100644
--- a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts
+++ b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts
@@ -1,5 +1,5 @@
import getRates from "$lib/rates";
-import { auth, fd, post } from "$lib/utils";
+import { auth, fd, get, post } from "$lib/utils";
import { fail, redirect } from "@sveltejs/kit";
export async function load({ cookies, params, parent }) {
@@ -11,6 +11,16 @@ export async function load({ cookies, params, parent }) {
}
data.rate = rates[user.currency];
+
+ const aid = cookies.get("aid") || user.id;
+ if (aid !== user.id) {
+ const account = await get(`/account/${aid}`, auth(cookies));
+ if (account.type === "ark") {
+ data.account = account;
+ data.serverArkAddress = await get("/ark/address");
+ }
+ }
+
return data;
}
@@ -36,6 +46,7 @@ export const actions = {
let p;
try {
const body = await fd(request);
+ body.aid = cookies.get("aid");
console.log("BODY", body);
diff --git a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte
index 4dfaabc2..02ff06ae 100644
--- a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte
+++ b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte
@@ -2,12 +2,14 @@
import { untrack } from "svelte";
import { t } from "$lib/translations";
import { enhance } from "$app/forms";
+ import { goto } from "$app/navigation";
import Amount from "$comp/Amount.svelte";
import Numpad from "$comp/Numpad.svelte";
import Spinner from "$comp/Spinner.svelte";
import { page } from "$app/stores";
- import { loc, back, toFiat, f, s, focus } from "$lib/utils";
+ import { loc, back, toFiat, f, s, post, focus } from "$lib/utils";
import { fiat, rate, pin } from "$lib/store";
+ import { sendArk } from "$lib/ark";
let { data, form } = $props();
@@ -20,9 +22,9 @@
$effect(() => form && (loading = false));
let showMax = $state();
+ let error = $state("");
let loading = $state();
- let submit = () => (loading = true);
let next = $state();
let toggle = () => (show = !show);
@@ -31,16 +33,49 @@
Math.max(5, Math.round(untrack(() => amount) * 0.02 || 0)),
);
$effect(() => (maxfee = Math.max(5, Math.round(amount * 0.02) || 0)));
+
+ let handler = ({ cancel }) => {
+ loading = true;
+ error = "";
+
+ if (data.account?.type === "ark") {
+ cancel();
+
+ (async () => {
+ try {
+ const txid = await sendArk(data.serverArkAddress, parseInt(amount));
+
+ const inv = await post("/post/invoice", {
+ invoice: { type: "ark", amount: parseInt(amount), forward: payreq, aid: data.account.id },
+ user,
+ });
+
+ const p = await post("/post/ark/receive", {
+ amount: parseInt(amount),
+ hash: txid,
+ iid: inv.id,
+ });
+
+ goto(`/sent/${p.id}`, { invalidateAll: true });
+ } catch (e) {
+ loading = false;
+ error = e.message || "Failed to send";
+ }
+ })();
+
+ return;
+ }
+ };
</script>
<div class="container px-4 max-w-xl mx-auto text-center space-y-2">
- {#if form?.message}
+ {#if form?.message || error}
<div class="text-red-600 text-center">
- {#if form.message.includes($t("payments.insufficientFunds"))}
- <div>{form.message}</div>
- <div>{$t("payments.lockedBalance")}: {locked}</div>
+ {#if (form?.message || error).includes($t("payments.insufficientFunds"))}
+ <div>{form?.message || error}</div>
+ <div>{$t("payments.lockedBalance")}: {locked}</div>
{:else}
- {$t("payments.failedToRoute")}
+ {form?.message || error}
{/if}
</div>
{/if}
@@ -78,8 +113,7 @@
<form
method="POST"
- use:enhance
- onsubmit={submit}
+ use:enhance={handler}
action="?/send"
class="space-y-2"
>