aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-09 01:19:24 -0800
committerAdam Soltys <[email protected]>2026-02-11 15:34:34 -0800
commit8aad862255f18f80de9547eb14d2514aad16b783 (patch)
tree93f6ef16483735a20be1941e818465d3a77cebf6 /src
parentf85f5bebc49f566eaeb7bd81a285b6516dbefa9f (diff)
downloadcoinos-ui-8aad862255f18f80de9547eb14d2514aad16b783.tar.gz
coinos-ui-8aad862255f18f80de9547eb14d2514aad16b783.zip
ark fixes
Diffstat (limited to 'src')
-rw-r--r--src/lib/ark.ts48
-rw-r--r--src/routes/(app)/+layout.svelte89
-rw-r--r--src/routes/(app)/send/ark/[address]/+page.svelte14
-rw-r--r--src/routes/(app)/send/ark/[address]/[amount]/[...feeRate]/+page.svelte56
4 files changed, 93 insertions, 114 deletions
diff --git a/src/lib/ark.ts b/src/lib/ark.ts
index 25122a8c..ff82393b 100644
--- a/src/lib/ark.ts
+++ b/src/lib/ark.ts
@@ -8,16 +8,27 @@ export const arkkey = persistLocal("arkkey", "");
let arkSdkPromise: Promise<typeof import("@arkade-os/sdk")> | undefined;
const loadArkSdk = () => (arkSdkPromise ||= import("@arkade-os/sdk"));
+let walletInstance: any;
+let walletKey: string | undefined;
+
export const getWallet = async () => {
const key = get(arkkey);
- if (!key) return;
+ if (!key) {
+ walletInstance = undefined;
+ walletKey = undefined;
+ return;
+ }
+ if (walletInstance && walletKey === key) return walletInstance;
+
const { SingleKey, Wallet } = await loadArkSdk();
const identity = SingleKey.fromHex(key);
- return Wallet.create({
+ walletInstance = await Wallet.create({
identity,
arkServerUrl,
});
+ walletKey = key;
+ return walletInstance;
};
export const getAddress = async (): Promise<string> => {
@@ -44,7 +55,8 @@ export const syncTransactions = async (aid: string) => {
if (!history.length) return;
const transactions = history.map((tx) => ({
- hash: tx.key.arkTxid || tx.key.commitmentTxid,
+ arkTxid: tx.key.arkTxid || null,
+ commitmentTxid: tx.key.commitmentTxid || null,
amount: tx.type === "RECEIVED" ? tx.amount : -tx.amount,
settled: tx.settled,
createdAt: tx.createdAt,
@@ -73,18 +85,29 @@ export const addProcessedVtxos = (keys: string[]) => {
localStorage.setItem("processedArkVtxos", JSON.stringify(arr));
};
+// Flag to suppress notifyIncomingFunds during sends
+export let arkSending = false;
+
export const sendArk = async (address: string, amount: number) => {
const wallet = await getWallet();
if (!wallet) throw new Error("Ark wallet not available");
- const txid = await wallet.sendBitcoin({ address, amount });
+ // Suppress incoming notifications during send to prevent
+ // change VTXOs from being credited as incoming payments
+ arkSending = true;
- // 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));
+ try {
+ 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;
+ return txid;
+ } finally {
+ arkSending = false;
+ }
};
export const settle = async () => {
@@ -94,5 +117,10 @@ export const settle = async () => {
const balance = await wallet.getBalance();
if (!balance.preconfirmed) return;
- return wallet.settle();
+ await wallet.settle();
+
+ // Mark post-settle VTXOs as processed so notifyIncomingFunds
+ // ignores renewed VTXOs (they get new txids after an Ark round)
+ const vtxos = await wallet.getVtxos({ spendableOnly: false });
+ if (vtxos?.length) addProcessedVtxos(vtxos.map(vtxoKey));
};
diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte
index 587a4b98..b67194b7 100644
--- a/src/routes/(app)/+layout.svelte
+++ b/src/routes/(app)/+layout.svelte
@@ -1,5 +1,5 @@
<script>
- import { getWallet, getAddress, getVtxos, syncTransactions, settle, vtxoKey, getProcessedVtxos, addProcessedVtxos } from "$lib/ark";
+ import { getWallet, syncTransactions, settle } from "$lib/ark";
import { SvelteToast } from "@zerodevx/svelte-toast";
import { onDestroy, onMount } from "svelte";
import { close, connect, send, socket } from "$lib/socket";
@@ -22,7 +22,7 @@
import AppHeader from "$comp/AppHeader.svelte";
import Nostr from "$comp/Nostr.svelte";
import Password from "$comp/Password.svelte";
- import { s, f, toFiat, success, post, getCookie, warning } from "$lib/utils";
+ import { s, f, toFiat, success, getCookie, warning } from "$lib/utils";
import { t, locale, loading } from "$lib/translations";
import { goto, invalidate, afterNavigate, preloadData } from "$app/navigation";
@@ -46,68 +46,29 @@
}
});
- // Prevent concurrent processing of Ark notifications
- let arkProcessing = false;
+ let arkSyncing = false;
- let handleArkPayment = async (notification) => {
- // Skip if already processing (prevents race conditions from rapid SDK callbacks)
- if (arkProcessing) return;
- arkProcessing = true;
-
- console.log("ARK payment notification:", notification);
-
- // Filter out already-processed VTXOs using txid:vout composite key
- let processed = getProcessedVtxos();
- let newVtxos = notification.newVtxos.filter((v) => !processed.has(vtxoKey(v)));
-
- if (newVtxos.length === 0) {
- arkProcessing = false;
- return;
- }
-
- // Sum only truly new VTXOs
- let amount = newVtxos.reduce((a, b) => a + b.value, 0);
-
- // Mark these VTXOs as processed immediately
- addProcessedVtxos(newVtxos.map(vtxoKey));
-
- // Only process positive incoming amounts
- if (amount <= 0) {
- arkProcessing = false;
- return;
- }
-
- if ($fiat && $rateStore && user?.currency) {
- success(`Received ${f(toFiat(amount, $rateStore), user.currency)}!`);
- } else {
- success(`Received ⚡️${s(amount)}!`);
- }
+ let arkSync = async () => {
+ if (arkSyncing) return;
+ arkSyncing = true;
try {
- let aid = getCookie("aid") || user.id;
-
- let inv = await post(`/post/invoice`, {
- invoice: { type: "ark", amount, aid },
- user,
- });
-
- let hash = newVtxos[0]?.txid;
+ const aid = getCookie("aid") || user.id;
+ const result = await syncTransactions(aid);
- await post(`/post/ark/receive`, {
- amount,
- hash,
- iid: inv.id,
- });
-
- goto(`/invoice/${inv.id}`, {
- invalidateAll: true,
- noScroll: true,
- });
+ if (result?.received > 0) {
+ if ($fiat && $rateStore && user?.currency) {
+ success(`Received ${f(toFiat(result.received, $rateStore), user.currency)}!`);
+ } else {
+ success(`Received ⚡️${s(result.received)}!`);
+ }
+ }
+ if (result?.synced) invalidate("app:payments");
} catch (e) {
- console.error("Failed to record ARK payment:", e);
+ console.error("Ark sync failed:", e);
} finally {
- arkProcessing = false;
+ arkSyncing = false;
}
};
@@ -119,19 +80,9 @@
if (user) {
const wallet = await getWallet();
if (wallet) {
- // Pre-populate processed VTXOs with existing ones before listening
- const existingVtxos = await getVtxos();
- if (existingVtxos?.length) {
- addProcessedVtxos(existingVtxos.map(vtxoKey));
- console.log("Pre-populated", existingVtxos.length, "existing VTXOs");
- }
-
- wallet.notifyIncomingFunds(handleArkPayment);
+ wallet.notifyIncomingFunds(arkSync);
- const aid = getCookie("aid") || user.id;
- syncTransactions(aid).then((result) => {
- if (result?.synced) invalidate("app:payments");
- }).catch((e) => console.error("Ark sync failed:", e));
+ arkSync();
settle().catch((e) => console.error("Ark settle failed:", e));
}
diff --git a/src/routes/(app)/send/ark/[address]/+page.svelte b/src/routes/(app)/send/ark/[address]/+page.svelte
index 140a2393..5361f292 100644
--- a/src/routes/(app)/send/ark/[address]/+page.svelte
+++ b/src/routes/(app)/send/ark/[address]/+page.svelte
@@ -5,8 +5,6 @@
import { page } from "$app/stores";
import { fiat as fiatStore, rate } from "$lib/store";
import { loc, fail, s, f, sats } from "$lib/utils";
- import { getWallet } from "$lib/ark";
-
let { data } = $props();
let { balance, user } = data;
@@ -30,16 +28,6 @@
submit.click();
};
- let send = async () => {
- console.log("SENDING", a);
- let wallet = await getWallet();
- const txid = await wallet.sendBitcoin({
- address,
- amount,
- });
-
- console.log(txid);
- };
</script>
<div class="container px-4 max-w-xl mx-auto space-y-5 text-center">
@@ -72,10 +60,8 @@
<form
action={`/send/ark/${address}/${amount}`}
class="contents"
- onsubmit={send}
>
<button
- use:focus
bind:this={submit}
type="submit"
class="btn !w-auto grow btn-accent"
diff --git a/src/routes/(app)/send/ark/[address]/[amount]/[...feeRate]/+page.svelte b/src/routes/(app)/send/ark/[address]/[amount]/[...feeRate]/+page.svelte
index 92bddd15..95812266 100644
--- a/src/routes/(app)/send/ark/[address]/[amount]/[...feeRate]/+page.svelte
+++ b/src/routes/(app)/send/ark/[address]/[amount]/[...feeRate]/+page.svelte
@@ -2,7 +2,7 @@
import { t } from "$lib/translations";
import { enhance } from "$app/forms";
import Spinner from "$comp/Spinner.svelte";
- import { focus, s } from "$lib/utils";
+ import { focus, s, post } from "$lib/utils";
import { pin } from "$lib/store";
import { goto, invalidate } from "$app/navigation";
import { rate } from "$lib/store";
@@ -15,30 +15,44 @@
let error = $state("");
- let handler = async ({ cancel }) => {
- submitting = true;
- error = "";
+ let handler = ({ cancel }) => {
+ if (account.type === "ark") {
+ cancel();
- try {
- if (account.type === "ark") {
- const txid = await sendArk(address, amount);
- console.log("ARK tx:", txid);
- }
+ (async () => {
+ submitting = true;
+ error = "";
+ try {
+ const txid = await sendArk(address, parseInt(amount));
- return async ({ result }) => {
- submitting = false;
- if (result.type === "redirect") {
- goto(result.location);
- } else {
- await applyAction(result);
- invalidate("app:payments");
+ const p = await post("/post/ark/vault-send", {
+ hash: txid,
+ amount: parseInt(amount),
+ aid: account.id,
+ });
+
+ goto(`/sent/${p.id}`, { invalidateAll: true });
+ } catch (e) {
+ submitting = false;
+ error = e.message || "Failed to send";
}
- };
- } catch (e) {
- submitting = false;
- error = e.message || "Failed to send";
- cancel();
+ })();
+
+ return;
}
+
+ submitting = true;
+ error = "";
+
+ return async ({ result }) => {
+ submitting = false;
+ if (result.type === "redirect") {
+ goto(result.location);
+ } else {
+ await applyAction(result);
+ invalidate("app:payments");
+ }
+ };
};
let { account, amount, address, message } = $derived(data);