clean code, and use newly created PortalBtcWallet library
This commit is contained in:
parent
9825c53c1c
commit
45dfe1a1c7
22 changed files with 503 additions and 1087 deletions
|
@ -13,20 +13,19 @@
|
|||
"lint": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@breeztech/breez-sdk-liquid": "^0.9.2-rc2",
|
||||
"@cashu/cashu-ts": "^2.5.2",
|
||||
"@libsql/client": "^0.15.9",
|
||||
"@scure/base": "^1.2.6",
|
||||
"@scure/bip39": "^1.6.0",
|
||||
"@sveltejs/adapter-auto": "^6.0.1",
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"dexie": "^4.0.11",
|
||||
"iconify-icon": "^3.0.0",
|
||||
"nostr-tools": "^2.15.0",
|
||||
"portalbtc-lib": "git+ssh://git@git.arx-ccn.com:222/Arx/PortalBTCLib.git#2d858727b05f9d66e4f028c086d93cebf769f3b8",
|
||||
"qrcode": "^1.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import { getMnemonic } from "$lib/wallet.svelte";
|
||||
import initBreez, {
|
||||
type BindingLiquidSdk,
|
||||
connect as breezConnect,
|
||||
defaultConfig as defaultBreezConfig,
|
||||
} from "@breeztech/breez-sdk-liquid/web";
|
||||
|
||||
initBreez();
|
||||
|
||||
let breezSDK: BindingLiquidSdk;
|
||||
let initialized = false;
|
||||
|
||||
export async function getBreezSDK() {
|
||||
if (initialized) return breezSDK;
|
||||
|
||||
const mnemonic = await getMnemonic();
|
||||
|
||||
breezSDK = await breezConnect({
|
||||
mnemonic,
|
||||
config: defaultBreezConfig(
|
||||
"mainnet",
|
||||
"MIIBajCCARygAwIBAgIHPgwAQY4DlTAFBgMrZXAwEDEOMAwGA1UEAxMFQnJlZXowHhcNMjUwNTA1MTY1OTM5WhcNMzUwNTAzMTY1OTM5WjAnMQwwCgYDVQQKEwNBcngxFzAVBgNVBAMTDkRhbm55IE1vcmFiaXRvMCowBQYDK2VwAyEA0IP1y98gPByiIMoph1P0G6cctLb864rNXw1LRLOpXXejfjB8MA4GA1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTaOaPuXmtLDTJVv++VYBiQr9gHCTAfBgNVHSMEGDAWgBTeqtaSVvON53SSFvxMtiCyayiYazAcBgNVHREEFTATgRFkYW5ueUBhcngtY2NuLmNvbTAFBgMrZXADQQAwJoh9BG8rEH1sOl+BpS12oNSwzgQga8ZcIAZ8Bjmd6QT4GSST0nLj06fs49pCkiULOl9ZoRIeIMc3M1XqV5UA",
|
||||
), // this key can be shared, it's fine
|
||||
});
|
||||
|
||||
breezSDK.addEventListener({
|
||||
onEvent: (e) => {
|
||||
console.log(e);
|
||||
refreshBalances();
|
||||
},
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
return breezSDK;
|
||||
}
|
||||
|
||||
export async function initBalances() {
|
||||
const sdk = await getBreezSDK();
|
||||
const info = await sdk.getInfo();
|
||||
balances.balance = info.walletInfo.balanceSat;
|
||||
balances.pendingReceive = info.walletInfo.pendingReceiveSat;
|
||||
balances.pendingSend = info.walletInfo.pendingSendSat;
|
||||
}
|
||||
|
||||
export const balances = $state({
|
||||
balance: 0,
|
||||
pendingReceive: 0,
|
||||
pendingSend: 0,
|
||||
tick: 0,
|
||||
});
|
||||
|
||||
export async function refreshBalances() {
|
||||
const sdk = await getBreezSDK();
|
||||
const info = await sdk.getInfo();
|
||||
balances.balance = info.walletInfo.balanceSat;
|
||||
balances.pendingReceive = info.walletInfo.pendingReceiveSat;
|
||||
balances.pendingSend = info.walletInfo.pendingSendSat;
|
||||
balances.tick++;
|
||||
}
|
|
@ -1,393 +0,0 @@
|
|||
import {
|
||||
CashuMint,
|
||||
CashuWallet,
|
||||
getDecodedToken,
|
||||
MintQuoteState,
|
||||
type Proof,
|
||||
} from "@cashu/cashu-ts";
|
||||
import { get, writable } from "svelte/store";
|
||||
import { getMnemonic } from "$lib/wallet.svelte";
|
||||
import { privateKeyFromSeedWords as nostrPrivateKeyFromSeedWords } from "nostr-tools/nip06";
|
||||
import { getBreezSDK } from "$lib/breez.svelte";
|
||||
import { finalizeEvent, getPublicKey, nip19, nip98 } from "nostr-tools";
|
||||
import Dexie, { type Table } from "dexie";
|
||||
import { BASE_DOMAIN } from "$lib/config";
|
||||
|
||||
interface MetaEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface CashuTxn {
|
||||
txId: string;
|
||||
paymentType: "receive" | "send";
|
||||
amountSat: number;
|
||||
timestamp: number;
|
||||
status: "complete";
|
||||
}
|
||||
|
||||
class CashuDB extends Dexie {
|
||||
proofs!: Table<Proof, string>;
|
||||
meta!: Table<MetaEntry, string>;
|
||||
txns!: Table<CashuTxn, string>;
|
||||
|
||||
constructor() {
|
||||
super("cashu");
|
||||
this.version(1).stores({
|
||||
proofs: "&secret",
|
||||
meta: "&key",
|
||||
});
|
||||
this.version(2).stores({
|
||||
txns: "&txId",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const cashuDB = new CashuDB();
|
||||
|
||||
export type { CashuTxn };
|
||||
|
||||
const MINT_URL = "https://mint.minibits.cash/Bitcoin";
|
||||
|
||||
interface CashuState {
|
||||
balance: number;
|
||||
meltThreshold: number;
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
export const cashuState = writable<CashuState>({
|
||||
balance: 0,
|
||||
meltThreshold: 2000,
|
||||
lastUpdated: 0,
|
||||
});
|
||||
|
||||
let proofs: Proof[] = [];
|
||||
|
||||
async function loadProofs() {
|
||||
proofs = await cashuDB.proofs.toArray();
|
||||
updateBalance();
|
||||
}
|
||||
|
||||
async function persistProofs() {
|
||||
await cashuDB.proofs.clear();
|
||||
if (proofs.length) await cashuDB.proofs.bulkPut(proofs);
|
||||
}
|
||||
|
||||
async function fetchWithNip98<T>(
|
||||
{ url, method, body }: {
|
||||
url: string;
|
||||
method: string;
|
||||
body?: BodyInit | null;
|
||||
},
|
||||
): Promise<T> {
|
||||
const mnemonic = await getMnemonic();
|
||||
const nostrPrivateKey = nostrPrivateKeyFromSeedWords(mnemonic);
|
||||
const urlWithoutQueryParams = url.split("?")[0];
|
||||
const npubCashNip98 = await nip98.getToken(
|
||||
urlWithoutQueryParams,
|
||||
method,
|
||||
(event) => finalizeEvent(event, nostrPrivateKey),
|
||||
);
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
"Authorization": `Nostr ${npubCashNip98}`,
|
||||
},
|
||||
body,
|
||||
}).then((data) => data.json());
|
||||
}
|
||||
|
||||
export async function getNpub() {
|
||||
const mnemonic = await getMnemonic();
|
||||
const nostrPrivateKey = nostrPrivateKeyFromSeedWords(mnemonic);
|
||||
const nostrPubkey = getPublicKey(nostrPrivateKey);
|
||||
return nip19.npubEncode(nostrPubkey);
|
||||
}
|
||||
|
||||
export async function getCashuAddress() {
|
||||
return `${await getNpub()}@${BASE_DOMAIN}`;
|
||||
}
|
||||
|
||||
interface NpubCashQuote {
|
||||
createdAt: number;
|
||||
paidAt?: number;
|
||||
expiresAt: number;
|
||||
mintUrl: string;
|
||||
quoteId: string;
|
||||
request: string;
|
||||
amount: number;
|
||||
state: "PAID" | "ISSUED" | "INFLIGHT";
|
||||
locked: boolean;
|
||||
}
|
||||
|
||||
interface PaginationMetadata {
|
||||
total: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
async function tryRedeemUnredeemedCashuQuotes() {
|
||||
const lastRedeemedCashuQuoteTimestamp =
|
||||
localStorage.getItem("lastRedeemedCashuQuoteTimestamp") || 0;
|
||||
let quotes: NpubCashQuote[] = [];
|
||||
while (true) {
|
||||
const currentQuotes = await fetchWithNip98<
|
||||
{
|
||||
error: false;
|
||||
data: { quotes: NpubCashQuote[] };
|
||||
metadata: PaginationMetadata;
|
||||
} | {
|
||||
error: true;
|
||||
message: string;
|
||||
}
|
||||
>({
|
||||
url:
|
||||
`https://npubx.cash/api/v2/wallet/quotes?since=${lastRedeemedCashuQuoteTimestamp}&limit=50&offset=${quotes.length}`,
|
||||
method: "GET",
|
||||
});
|
||||
if (currentQuotes.error === false) {
|
||||
quotes.push(...currentQuotes.data.quotes);
|
||||
if (quotes.length >= currentQuotes.metadata.total) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
throw new Error(currentQuotes.message);
|
||||
}
|
||||
}
|
||||
quotes = quotes.sort((a, b) => a.createdAt - b.createdAt);
|
||||
for (const quote of quotes) {
|
||||
if (quote.state === "PAID") {
|
||||
const mint = new CashuMint(quote.mintUrl);
|
||||
const wallet = new CashuWallet(mint);
|
||||
const req = await mint.checkMintQuote(quote.quoteId);
|
||||
if (req.state === MintQuoteState.PAID && quote.paidAt) {
|
||||
const newProofs = await wallet.mintProofs(quote.amount, quote.quoteId);
|
||||
proofs.push(...newProofs);
|
||||
await persistProofs();
|
||||
const amountReceived = newProofs.reduce((sum, p) => sum + p.amount, 0);
|
||||
cashuTxns.update((txs) => {
|
||||
txs.push({
|
||||
txId: `cashu-quote-${quote.quoteId}`,
|
||||
paymentType: "receive",
|
||||
amountSat: amountReceived,
|
||||
timestamp: quote.paidAt
|
||||
? Math.floor(quote.paidAt)
|
||||
: Math.floor(Date.now() / 1000),
|
||||
status: "complete",
|
||||
});
|
||||
return txs;
|
||||
});
|
||||
persistCashuTxns();
|
||||
updateBalance();
|
||||
localStorage.setItem(
|
||||
"lastRedeemedCashuQuoteTimestamp",
|
||||
quote.paidAt.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return quotes;
|
||||
}
|
||||
|
||||
let wallet: CashuWallet | null = null;
|
||||
let walletPromise: Promise<CashuWallet> | null = null;
|
||||
|
||||
export async function getWallet(): Promise<CashuWallet> {
|
||||
if (wallet) return wallet;
|
||||
if (walletPromise) return walletPromise;
|
||||
|
||||
walletPromise = (async () => {
|
||||
try {
|
||||
await loadProofs();
|
||||
try {
|
||||
await getMnemonic();
|
||||
} catch (_) {}
|
||||
|
||||
const mint = new CashuMint(MINT_URL);
|
||||
const w = new CashuWallet(mint);
|
||||
await w.loadMint();
|
||||
|
||||
tryRedeemUnredeemedCashuQuotes()
|
||||
.then(() => persistProofs())
|
||||
.catch((err) => console.error("Failed background redemption", err));
|
||||
|
||||
getCashuAddress()
|
||||
.then((addr) => console.log(`cashu addr: ${addr}`))
|
||||
.catch(() => {});
|
||||
|
||||
wallet = w;
|
||||
return w;
|
||||
} catch (err) {
|
||||
walletPromise = null;
|
||||
throw err;
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
if (wallet) walletPromise = null;
|
||||
}, 0);
|
||||
}
|
||||
})();
|
||||
|
||||
return walletPromise;
|
||||
}
|
||||
|
||||
let quoteRedeemInterval: ReturnType<typeof setInterval> | undefined;
|
||||
if (quoteRedeemInterval) clearInterval(quoteRedeemInterval);
|
||||
quoteRedeemInterval = setInterval(
|
||||
() => tryRedeemUnredeemedCashuQuotes().then(maybeMelt),
|
||||
1000 * 5,
|
||||
);
|
||||
|
||||
function updateBalance() {
|
||||
const balance = proofs.reduce((sum, p) => sum + p.amount, 0);
|
||||
cashuState.update((s) => ({ ...s, balance, lastUpdated: s.lastUpdated + 1 }));
|
||||
}
|
||||
|
||||
export async function redeemToken(token: string): Promise<void> {
|
||||
if (!token.trim()) throw new Error("Token is empty");
|
||||
|
||||
const parsedToken = getDecodedToken(token);
|
||||
const mint = new CashuMint(parsedToken.mint);
|
||||
const wallet = new CashuWallet(mint);
|
||||
const received = await wallet.receive(token.trim());
|
||||
proofs.push(...received);
|
||||
const amountReceived = received.reduce((sum, p) => sum + p.amount, 0);
|
||||
cashuTxns.update((txs) => {
|
||||
txs.push({
|
||||
txId: `cashu-receive-${Date.now()}`,
|
||||
paymentType: "receive",
|
||||
amountSat: amountReceived,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
status: "complete",
|
||||
});
|
||||
return txs;
|
||||
});
|
||||
persistCashuTxns();
|
||||
updateBalance();
|
||||
await persistProofs();
|
||||
await maybeMelt();
|
||||
}
|
||||
|
||||
type PrepareReceiveRequest = {
|
||||
paymentMethod: "bolt11Invoice";
|
||||
amountSat: number;
|
||||
};
|
||||
|
||||
async function createMeltInvoice(amountSat: number): Promise<string> {
|
||||
const breezSDK = await getBreezSDK();
|
||||
const prepare = await breezSDK.prepareReceivePayment({
|
||||
paymentMethod: "bolt11Invoice",
|
||||
amount: { type: "bitcoin", payerAmountSat: amountSat },
|
||||
amountSat: amountSat,
|
||||
} as PrepareReceiveRequest);
|
||||
|
||||
const { destination } = await breezSDK.receivePayment({
|
||||
prepareResponse: prepare,
|
||||
});
|
||||
return destination as string;
|
||||
}
|
||||
|
||||
let tryingToMelt = false;
|
||||
async function maybeMelt() {
|
||||
if (tryingToMelt) return;
|
||||
tryingToMelt = true;
|
||||
const { balance, meltThreshold } = get(cashuState);
|
||||
if (balance < meltThreshold) return;
|
||||
|
||||
try {
|
||||
const wallet = await getWallet();
|
||||
const invoice = await createMeltInvoice(balance - (balance % 2000));
|
||||
const meltQuote = await wallet.createMeltQuote(invoice);
|
||||
|
||||
const amountToMelt = meltQuote.amount + meltQuote.fee_reserve;
|
||||
|
||||
const { keep, send } = await wallet.send(amountToMelt, proofs, {
|
||||
includeFees: true,
|
||||
});
|
||||
|
||||
proofs = keep;
|
||||
await persistProofs();
|
||||
|
||||
const { change } = await wallet.meltProofs(meltQuote, send);
|
||||
proofs.push(...change);
|
||||
await persistProofs();
|
||||
|
||||
cashuTxns.update((txs) => {
|
||||
txs.push({
|
||||
txId: `cashu-send-${Date.now()}`,
|
||||
paymentType: "send",
|
||||
amountSat: amountToMelt,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
status: "complete",
|
||||
});
|
||||
return txs;
|
||||
});
|
||||
persistCashuTxns();
|
||||
|
||||
updateBalance();
|
||||
} catch (err) {
|
||||
console.error("Failed to melt Cashu balance", err);
|
||||
}
|
||||
tryingToMelt = false;
|
||||
}
|
||||
|
||||
export const cashuTxns = writable<CashuTxn[]>([]);
|
||||
|
||||
async function loadCashuTxns() {
|
||||
try {
|
||||
const txns = await cashuDB.txns.toArray();
|
||||
cashuTxns.set(txns);
|
||||
} catch (err) {
|
||||
console.error("Failed to load Cashu txns", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function persistCashuTxns() {
|
||||
try {
|
||||
const txns = get(cashuTxns);
|
||||
await cashuDB.txns.clear();
|
||||
if (txns.length) await cashuDB.txns.bulkPut(txns);
|
||||
} catch (err) {
|
||||
console.error("Failed to persist Cashu txns", err);
|
||||
}
|
||||
}
|
||||
|
||||
loadCashuTxns();
|
||||
|
||||
export async function payBolt11Invoice(invoice: string): Promise<void> {
|
||||
if (!invoice.trim()) throw new Error("Invoice is empty");
|
||||
|
||||
const wallet = await getWallet();
|
||||
const meltQuote = await wallet.createMeltQuote(invoice);
|
||||
|
||||
const amountToMelt = meltQuote.amount + meltQuote.fee_reserve;
|
||||
|
||||
const { balance } = get(cashuState);
|
||||
if (amountToMelt > balance) {
|
||||
throw new Error("Insufficient Cashu balance");
|
||||
}
|
||||
|
||||
const { keep, send } = await wallet.send(amountToMelt, proofs, {
|
||||
includeFees: true,
|
||||
});
|
||||
|
||||
proofs = keep;
|
||||
await persistProofs();
|
||||
|
||||
const { change } = await wallet.meltProofs(meltQuote, send);
|
||||
|
||||
proofs.push(...change);
|
||||
await persistProofs();
|
||||
|
||||
cashuTxns.update((txs) => {
|
||||
txs.push({
|
||||
txId: `cashu-send-${Date.now()}`,
|
||||
paymentType: "send",
|
||||
amountSat: amountToMelt,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
status: "complete",
|
||||
});
|
||||
return txs;
|
||||
});
|
||||
persistCashuTxns();
|
||||
|
||||
updateBalance();
|
||||
}
|
|
@ -1,15 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { satsComma } from "$lib";
|
||||
import { cashuState } from "$lib/cashu.svelte";
|
||||
import { walletState } from "$lib/wallet.svelte";
|
||||
import LoadingIndicator from "./LoadingIndicator.svelte";
|
||||
|
||||
const { balance = 0, pending = 0 }: { balance: number; pending: number } =
|
||||
$props();
|
||||
let loading = $derived(!$walletState.open);
|
||||
let balance = $derived(
|
||||
$walletState.open ? $walletState.balance - $walletState.cashuBalance : 0
|
||||
);
|
||||
let pending = $derived($walletState.open ? $walletState.pendingBalance : 0);
|
||||
let cashuBalance = $derived(
|
||||
$walletState.open ? $walletState.cashuBalance : 0
|
||||
);
|
||||
|
||||
const formattedTotal = $derived(satsComma(balance + $cashuState.balance));
|
||||
const formattedTotal = $derived(satsComma(balance + cashuBalance));
|
||||
const formattedPending = $derived(satsComma(pending));
|
||||
const formattedLightning = $derived(satsComma(balance));
|
||||
const formattedCashu = $derived(satsComma($cashuState.balance));
|
||||
const cashuThreshold = $derived(satsComma($cashuState.meltThreshold));
|
||||
const formattedCashu = $derived(satsComma(cashuBalance));
|
||||
|
||||
let showSplit = $state(false);
|
||||
let containerEl: HTMLButtonElement;
|
||||
|
@ -38,24 +44,48 @@
|
|||
aria-label="Toggle balance split"
|
||||
>
|
||||
<div class="balance-label">Total Balance</div>
|
||||
<div class="balance-value">{formattedTotal} sats</div>
|
||||
<div class="balance-value">
|
||||
{#if loading}
|
||||
<LoadingIndicator />
|
||||
{:else}
|
||||
{formattedTotal} sats
|
||||
{/if}
|
||||
</div>
|
||||
<div class="pending-label">Pending</div>
|
||||
<div class="pending-value">{formattedPending} sats</div>
|
||||
<div class="pending-value">
|
||||
{#if loading}
|
||||
<LoadingIndicator />
|
||||
{:else}
|
||||
{formattedPending} sats
|
||||
{/if}
|
||||
</div>
|
||||
{#if showSplit}
|
||||
<div class="popover">
|
||||
<div class="split-row lightning">
|
||||
<iconify-icon icon="icon-park-twotone:lightning" width="32" height="32"
|
||||
></iconify-icon>
|
||||
<span class="label">Lightning</span>
|
||||
<span class="amount">{formattedLightning} sats</span>
|
||||
<span class="amount">
|
||||
{#if loading}
|
||||
<LoadingIndicator />
|
||||
{:else}
|
||||
{formattedLightning} sats
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<div class="split-row cashu">
|
||||
<iconify-icon icon="tdesign:nut-filled" width="32" height="32"
|
||||
></iconify-icon>
|
||||
<span class="label">Cashu</span>
|
||||
<span class="amount">{formattedCashu} sats</span>
|
||||
<span class="amount">
|
||||
{#if loading}
|
||||
<LoadingIndicator />
|
||||
{:else}
|
||||
{formattedCashu} sats
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<div class="threshold">Auto-melt at {cashuThreshold} sats</div>
|
||||
<div class="threshold">Auto-melt at 2000 sats</div>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<script lang="ts">
|
||||
import Button from "$lib/components/Button.svelte";
|
||||
import { redeemToken } from "$lib/cashu.svelte";
|
||||
import { walletState } from "$lib/wallet.svelte";
|
||||
|
||||
let tokenInput = $state("");
|
||||
|
||||
function redeem() {
|
||||
redeemToken(tokenInput).catch((e) => alert(e.message));
|
||||
if (!$walletState.open) return;
|
||||
$walletState.wallet.redeemToken(tokenInput).catch((e) => alert(e.message));
|
||||
tokenInput = "";
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,26 +1,16 @@
|
|||
<script lang="ts">
|
||||
import Button from "./Button.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { getMnemonic } from "$lib/wallet.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { walletState } from "$lib/wallet.svelte";
|
||||
|
||||
let seedPhrase = $state("");
|
||||
let showSeedPhrase = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
seedPhrase = await getMnemonic();
|
||||
} catch (err) {
|
||||
console.error("Failed to get mnemonic:", err);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleSeedPhrase() {
|
||||
showSeedPhrase = !showSeedPhrase;
|
||||
}
|
||||
|
||||
function copySeedPhrase() {
|
||||
navigator.clipboard.writeText(seedPhrase);
|
||||
navigator.clipboard.writeText($walletState.mnemonic!);
|
||||
}
|
||||
|
||||
function resetWebsite() {
|
||||
|
@ -57,7 +47,7 @@
|
|||
|
||||
{#if showSeedPhrase}
|
||||
<div class="seed-phrase">
|
||||
{seedPhrase}
|
||||
{$walletState.mnemonic!}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import Button from "./Button.svelte";
|
||||
import { BASE_DOMAIN } from "$lib/config";
|
||||
import { onMount } from "svelte";
|
||||
import { getNpub } from "$lib/cashu.svelte";
|
||||
import { walletState } from "$lib/wallet.svelte";
|
||||
|
||||
let username = $state("");
|
||||
let existingUsername = $state("");
|
||||
|
@ -17,7 +17,8 @@
|
|||
|
||||
async function checkExistingUsername() {
|
||||
try {
|
||||
const userNpub = await getNpub();
|
||||
if (!$walletState.open) return;
|
||||
const userNpub = $walletState.wallet.npub;
|
||||
const response = await fetch(
|
||||
`/api/check-username?npub=${encodeURIComponent(userNpub)}`
|
||||
);
|
||||
|
@ -45,7 +46,8 @@
|
|||
registrationStatus = "";
|
||||
|
||||
try {
|
||||
const userNpub = await getNpub();
|
||||
if (!$walletState.open) return;
|
||||
const userNpub = $walletState.wallet.npub;
|
||||
const response = await fetch(
|
||||
`/.well-known/lnurlp/${encodeURIComponent(username)}`,
|
||||
{
|
||||
|
|
33
src/lib/components/LoadingIndicator.svelte
Normal file
33
src/lib/components/LoadingIndicator.svelte
Normal file
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
let { size = 32 }: { size?: number } = $props();
|
||||
</script>
|
||||
|
||||
<div class="loading-indicator">
|
||||
<div class="loading-indicator-inner" style:--size={size}></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.loading-indicator {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-indicator-inner {
|
||||
border: 8px solid var(--primary-color);
|
||||
border-top: 8px solid var(--accent-color);
|
||||
border-radius: 50%;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,26 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { decryptMnemonic } from "$lib/wallet.svelte";
|
||||
import {
|
||||
decryptMnemonic,
|
||||
hasMnemonic,
|
||||
MNEMONIC_KEY,
|
||||
openWallet,
|
||||
} from "$lib/wallet.svelte";
|
||||
import { browser } from "$app/environment";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
unlock,
|
||||
}: { open: boolean; unlock: (pw: string) => void } = $props();
|
||||
let { onunlock }: { onunlock: () => void } = $props();
|
||||
|
||||
let dialogEl: HTMLDialogElement | null = $state(null);
|
||||
let password = $state("");
|
||||
let error = $state("");
|
||||
let isValidating = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (open) dialogEl?.showModal();
|
||||
else dialogEl?.close();
|
||||
onMount(() => {
|
||||
dialogEl?.showModal();
|
||||
});
|
||||
|
||||
function closeDialog() {
|
||||
open = false;
|
||||
}
|
||||
|
||||
async function attemptUnlock() {
|
||||
if (!password.trim()) return;
|
||||
|
||||
|
@ -29,23 +27,21 @@
|
|||
|
||||
try {
|
||||
if (!browser) throw new Error("Not in browser");
|
||||
const encryptedSeed = localStorage.getItem("seed");
|
||||
if (!encryptedSeed) throw new Error("No encrypted seed found");
|
||||
await decryptMnemonic(encryptedSeed, password);
|
||||
unlock(password);
|
||||
password = "";
|
||||
error = "";
|
||||
if (!hasMnemonic()) throw new Error("No encrypted seed found");
|
||||
const encryptedMnemonic = localStorage.getItem(MNEMONIC_KEY)!;
|
||||
const mnemonic = await decryptMnemonic(encryptedMnemonic, password);
|
||||
await openWallet(mnemonic);
|
||||
onunlock();
|
||||
dialogEl?.close();
|
||||
} catch (err) {
|
||||
error = "Incorrect password";
|
||||
console.error("Password validation failed:", err);
|
||||
} finally {
|
||||
isValidating = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<dialog bind:this={dialogEl} onclose={closeDialog}>
|
||||
<dialog bind:this={dialogEl}>
|
||||
<h2>Unlock Wallet</h2>
|
||||
<p>Enter your wallet password to decrypt your seed.</p>
|
||||
{#if error}
|
||||
|
@ -77,7 +73,6 @@
|
|||
>
|
||||
</div>
|
||||
</dialog>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
dialog {
|
||||
|
|
|
@ -1,42 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { balances, getBreezSDK } from "$lib/breez.svelte";
|
||||
import PaymentHistoryItem from "$lib/components/PaymentHistoryItem.svelte";
|
||||
import type {
|
||||
BindingLiquidSdk,
|
||||
Payment,
|
||||
} from "@breeztech/breez-sdk-liquid/web";
|
||||
import { cashuTxns } from "$lib/cashu.svelte";
|
||||
import LoadingIndicator from "$lib/components/LoadingIndicator.svelte";
|
||||
import { walletState } from "$lib/wallet.svelte";
|
||||
|
||||
let payments = $state<Payment[]>([]);
|
||||
|
||||
const combinedPayments = $derived(
|
||||
[...payments, ...$cashuTxns].sort((a, b) => b.timestamp - a.timestamp)
|
||||
let payments = $derived(
|
||||
$walletState.open
|
||||
? $walletState.wallet?.listPayments(100, 0)
|
||||
: Promise.resolve([])
|
||||
);
|
||||
|
||||
async function loadPayments() {
|
||||
try {
|
||||
const breezSDK = await getBreezSDK();
|
||||
const result = await breezSDK.listPayments({});
|
||||
payments = result;
|
||||
} catch (err) {
|
||||
console.error("Failed to load payments", err);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
balances.tick;
|
||||
loadPayments();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="payment-history">
|
||||
{#if combinedPayments.length === 0}
|
||||
{#await payments}
|
||||
<LoadingIndicator />
|
||||
{:then payments}
|
||||
{#if payments.length === 0}
|
||||
<p class="empty">No payments yet.</p>
|
||||
{:else}
|
||||
{#each combinedPayments as payment (payment.txId)}
|
||||
{#each payments as payment (payment.txId)}
|
||||
<PaymentHistoryItem {payment} />
|
||||
{/each}
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
import type { Payment } from "@breeztech/breez-sdk-liquid/web";
|
||||
import { satsComma } from "$lib";
|
||||
import { getBreezSDK } from "$lib/breez.svelte";
|
||||
import Button from "$lib/components/Button.svelte";
|
||||
import { walletState } from "$lib/wallet.svelte";
|
||||
|
||||
interface CashuPayment {
|
||||
txId: string;
|
||||
|
@ -29,60 +29,15 @@
|
|||
let isRefunding = $state(false);
|
||||
|
||||
async function refundPayment() {
|
||||
if (!$walletState.open) return;
|
||||
if (isRefunding) return;
|
||||
isRefunding = true;
|
||||
const breezSDK = await getBreezSDK();
|
||||
const prepareAddr = await breezSDK.prepareReceivePayment({
|
||||
paymentMethod: "bitcoinAddress",
|
||||
});
|
||||
const receiveRes = await breezSDK.receivePayment({
|
||||
prepareResponse: prepareAddr,
|
||||
});
|
||||
const refundAddress = receiveRes.destination as string;
|
||||
|
||||
if (payment.paymentType !== "send") return;
|
||||
if (payment.txId?.startsWith("cashu-")) return;
|
||||
try {
|
||||
const refundables = await breezSDK.listRefundables();
|
||||
let swapAddress: string | undefined;
|
||||
if (refundables.length === 1) {
|
||||
swapAddress = refundables[0].swapAddress;
|
||||
} else {
|
||||
swapAddress = refundables.find(
|
||||
(r) =>
|
||||
r.amountSat === payment.amountSat &&
|
||||
Math.abs(r.timestamp - payment.timestamp) < 300
|
||||
)?.swapAddress;
|
||||
}
|
||||
|
||||
if (!swapAddress) {
|
||||
alert("Could not identify refundable swap for this payment.");
|
||||
return;
|
||||
}
|
||||
|
||||
const fees = await breezSDK.recommendedFees();
|
||||
const feeRateSatPerVbyte = fees.economyFee;
|
||||
|
||||
const refundRequest = {
|
||||
swapAddress,
|
||||
refundAddress,
|
||||
feeRateSatPerVbyte,
|
||||
} as const;
|
||||
|
||||
try {
|
||||
await breezSDK.prepareRefund(refundRequest);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"prepareRefund failed (may be expected for some swaps)",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
await breezSDK.refund(refundRequest);
|
||||
alert(
|
||||
"Refund transaction broadcasted. It may take a moment to appear in history."
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Refund failed", err);
|
||||
alert(`Refund failed: ${err instanceof Error ? err.message : err}`);
|
||||
await $walletState.wallet.refundPayment(payment);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
isRefunding = false;
|
||||
}
|
||||
|
@ -105,7 +60,7 @@
|
|||
</a>
|
||||
{:else if (payment as Payment).details.type === "lightning"}
|
||||
<a
|
||||
href="https://liquid.network/tx/{payment.details.claimTxId}"
|
||||
href="https://liquid.network/tx/{payment.claimTxId}"
|
||||
target="_blank"
|
||||
>
|
||||
...{payment.txId?.slice(-16)}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
import Button from "$lib/components/Button.svelte";
|
||||
import Tabs from "$lib/components/Tabs.svelte";
|
||||
import CashuTab from "$lib/components/CashuTab.svelte";
|
||||
import ReceiveTab from "$lib/components/ReceiveTab.svelte";
|
||||
import { walletState } from "$lib/wallet.svelte";
|
||||
|
||||
let { open = $bindable() }: { open: boolean } = $props();
|
||||
|
||||
|
@ -23,27 +23,29 @@
|
|||
<div class="content">
|
||||
<h3>Receive</h3>
|
||||
|
||||
{#if $walletState.open}
|
||||
<Tabs labels={["Bitcoin", "Lightning", "Liquid", "Cashu"]}>
|
||||
{#snippet children(idx)}
|
||||
{#if idx === 0}
|
||||
<ReceiveTab type="bitcoin" alt="Bitcoin Address" />
|
||||
<ReceiveTab type="bitcoin" />
|
||||
{:else if idx === 1}
|
||||
<Tabs labels={["Address", "BOLT12"]}>
|
||||
{#snippet children(idx)}
|
||||
{#if idx === 0}
|
||||
<ReceiveTab type="lightning" alt="Lightning Address" />
|
||||
<ReceiveTab type="lightning" />
|
||||
{:else}
|
||||
<ReceiveTab type="bolt12" alt="Lightning Address" />
|
||||
<ReceiveTab type="bolt12" />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Tabs>
|
||||
{:else if idx === 2}
|
||||
<ReceiveTab type="liquid" alt="Liquid Address" />
|
||||
<ReceiveTab type="liquid" />
|
||||
{:else}
|
||||
<CashuTab />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Tabs>
|
||||
{/if}
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
|
|
@ -1,54 +1,47 @@
|
|||
<script lang="ts">
|
||||
import { getBreezSDK } from "$lib/breez.svelte";
|
||||
import { getCashuAddress } from "$lib/cashu.svelte";
|
||||
import { walletState } from "$lib/wallet.svelte";
|
||||
import Button from "$lib/components/Button.svelte";
|
||||
import QRCode from "qrcode";
|
||||
import { onMount } from "svelte";
|
||||
import LoadingIndicator from "$lib/components/LoadingIndicator.svelte";
|
||||
|
||||
const { type, alt }: { type: string; alt: string } = $props();
|
||||
const { type }: { type: string } = $props();
|
||||
|
||||
const paymentMethod = $derived(
|
||||
let paymentText = $state("");
|
||||
const loading = $derived(paymentText === "");
|
||||
|
||||
const qr = $derived(
|
||||
QRCode.toDataURL(`${type}:${paymentText}`, {
|
||||
errorCorrectionLevel: "H",
|
||||
})
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
if (!$walletState.open) return;
|
||||
const paymentMethod =
|
||||
type === "bitcoin"
|
||||
? "bitcoinAddress"
|
||||
: type === "bolt12"
|
||||
? "bolt12Offer"
|
||||
: type === "lightning"
|
||||
? "bolt11Invoice"
|
||||
: "liquidAddress"
|
||||
);
|
||||
: "liquidAddress";
|
||||
|
||||
const paymentText = $derived(
|
||||
paymentMethod === "bolt11Invoice"
|
||||
? getCashuAddress()
|
||||
: getBreezSDK().then((breezSDK) =>
|
||||
breezSDK.prepareReceivePayment({ paymentMethod }).then((prep) =>
|
||||
breezSDK.receivePayment({ prepareResponse: prep }).then((res) => {
|
||||
let destination = res.destination;
|
||||
if (destination.includes(":"))
|
||||
destination = destination.split(":")[1];
|
||||
if (destination.includes("?"))
|
||||
destination = destination.split("?")[0];
|
||||
return destination;
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const qr = $derived(
|
||||
paymentText.then((text) =>
|
||||
QRCode.toDataURL(`${type}:${text}`, {
|
||||
errorCorrectionLevel: "H",
|
||||
})
|
||||
)
|
||||
);
|
||||
if (paymentMethod === "bolt11Invoice") {
|
||||
paymentText = $walletState.wallet.lightningAddress;
|
||||
} else {
|
||||
paymentText = await $walletState.wallet.generateAddress(paymentMethod);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !loading}
|
||||
{#await qr}
|
||||
<div class="spinner"></div>
|
||||
<LoadingIndicator size={120} />
|
||||
{:then qrDataUrl}
|
||||
<img class="qr" src={qrDataUrl} alt="${alt}" />
|
||||
<img class="qr" src={qrDataUrl} alt={paymentText} />
|
||||
{/await}
|
||||
|
||||
{#await paymentText then paymentText}
|
||||
<pre class="addr">{paymentText}</pre>
|
||||
<Button
|
||||
class="copy-btn"
|
||||
|
@ -56,7 +49,9 @@
|
|||
>
|
||||
Copy
|
||||
</Button>
|
||||
{/await}
|
||||
{:else}
|
||||
<LoadingIndicator />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
img.qr {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { getBreezSDK } from "$lib/breez.svelte";
|
||||
import { send as sendPayment } from "$lib/payments";
|
||||
import Button from "$lib/components/Button.svelte";
|
||||
import type { InputType } from "@breeztech/breez-sdk-liquid/web";
|
||||
import { walletState } from "$lib/wallet.svelte";
|
||||
import { PaymentStatus } from "portalbtc-lib";
|
||||
|
||||
let { open = $bindable(), onsent }: { open: boolean; onsent: () => void } =
|
||||
$props();
|
||||
let { open = $bindable() }: { open: boolean } = $props();
|
||||
|
||||
let destination = $state("");
|
||||
|
||||
|
@ -21,8 +20,9 @@
|
|||
return;
|
||||
}
|
||||
try {
|
||||
const breezSDK = await getBreezSDK();
|
||||
const parsed: InputType = await breezSDK.parse(destination);
|
||||
if (!$walletState.open) return;
|
||||
const parsed: InputType =
|
||||
await $walletState.wallet.parsePayment(destination);
|
||||
if (
|
||||
parsed.type === "bolt11" &&
|
||||
parsed.invoice.amountMsat !== undefined &&
|
||||
|
@ -42,16 +42,51 @@
|
|||
let dialogEl: HTMLDialogElement;
|
||||
|
||||
async function handleSend() {
|
||||
const sendGenerator = sendPayment(destination, Number(amountSat) || 0);
|
||||
if (!$walletState.open) throw new Error("Wallet not open");
|
||||
const sendGenerator = $walletState.wallet.pay(
|
||||
destination,
|
||||
Number(amountSat) || 0
|
||||
);
|
||||
for await (const {
|
||||
status: sendStatus,
|
||||
error: sendError,
|
||||
} of sendGenerator) {
|
||||
if (sendError) return alert(sendError);
|
||||
status = sendStatus || "";
|
||||
if (sendError && sendStatus !== PaymentStatus.CashuPaymentFailed)
|
||||
return alert(sendError);
|
||||
switch (sendStatus) {
|
||||
case PaymentStatus.ParsingDestination:
|
||||
status = "Parsing destination...";
|
||||
break;
|
||||
case PaymentStatus.AttemptingCashuPayment:
|
||||
status = "Attempting cashu payment...";
|
||||
break;
|
||||
case PaymentStatus.AttemptingLightningPayment:
|
||||
status = "Attempting lightning payment...";
|
||||
break;
|
||||
case PaymentStatus.CashuPaymentFailed:
|
||||
status = "Cashu payment failed";
|
||||
break;
|
||||
case PaymentStatus.AmountRequired:
|
||||
status = "Amount required";
|
||||
break;
|
||||
case PaymentStatus.PreparingOnchainPayment:
|
||||
status = "Preparing onchain payment...";
|
||||
break;
|
||||
case PaymentStatus.BroadcastingOnchainPayment:
|
||||
status = "Broadcasting onchain payment...";
|
||||
break;
|
||||
case PaymentStatus.PaymentFailed:
|
||||
status = "Payment failed";
|
||||
break;
|
||||
case PaymentStatus.PaymentSent:
|
||||
status = "Payment sent";
|
||||
break;
|
||||
default:
|
||||
status = "Unknown status";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onsent();
|
||||
setTimeout(closeDialog, 1500);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { MAX_SATS_RECEIVE, MIN_SATS_RECEIVE } from "$lib/config";
|
||||
import { getDb } from "$lib/database";
|
||||
import { bech32 } from "@scure/base";
|
||||
import type { Dexie, Table } from "dexie";
|
||||
import { type Writable, writable } from "svelte/store";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
export function satsComma(sats: number) {
|
||||
const chars = sats.toString().split("").reverse();
|
||||
|
@ -74,3 +77,60 @@ export async function getUser(username: string) {
|
|||
);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
export function hexToBytes(hex: string): Uint8Array {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function bytesToHex(bytes: Uint8Array): string {
|
||||
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
export interface MetaEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function persistentDbWritable<T, DB extends Dexie>(
|
||||
key: string,
|
||||
initial: T,
|
||||
db: DB & { meta: Table<MetaEntry, string> },
|
||||
): Writable<T> {
|
||||
const store = writable<T>(initial);
|
||||
|
||||
if (browser) {
|
||||
(async () => {
|
||||
try {
|
||||
const entry = await db.meta.get(key);
|
||||
if (entry) {
|
||||
store.set(JSON.parse(entry.value));
|
||||
} else {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw !== null) {
|
||||
store.set(JSON.parse(raw));
|
||||
await db.meta.put({ key, value: raw });
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`NWC store load error for ${key}`, err);
|
||||
}
|
||||
})();
|
||||
|
||||
store.subscribe(async (value) => {
|
||||
try {
|
||||
await db.meta.put({ key, value: JSON.stringify(value) });
|
||||
} catch (err) {
|
||||
console.error(`NWC store persist error for ${key}`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
||||
|
|
|
@ -4,23 +4,19 @@ import {
|
|||
generateSecretKey,
|
||||
getPublicKey,
|
||||
nip04,
|
||||
nip19,
|
||||
SimplePool,
|
||||
verifyEvent,
|
||||
} from "nostr-tools";
|
||||
import type { Event as NostrEvent } from "nostr-tools";
|
||||
import { derived, get, type Writable, writable } from "svelte/store";
|
||||
import { derived, get } from "svelte/store";
|
||||
import { browser } from "$app/environment";
|
||||
import { getBreezSDK, refreshBalances } from "$lib/breez.svelte";
|
||||
import type { BindingLiquidSdk } from "@breeztech/breez-sdk-liquid/web";
|
||||
import {
|
||||
bytesToHex,
|
||||
hexToBytes,
|
||||
type MetaEntry,
|
||||
persistentDbWritable,
|
||||
} from "./utils";
|
||||
import { cashuDB, cashuState, getNpub } from "./cashu.svelte";
|
||||
import { send } from "./payments";
|
||||
} from "$lib";
|
||||
import { walletState } from "$lib/wallet.svelte";
|
||||
|
||||
interface PaidInvoice {
|
||||
paymentHash: string;
|
||||
|
@ -81,8 +77,7 @@ interface JsonRpcReq {
|
|||
| "get_balance"
|
||||
| "list_transactions"
|
||||
| "get_info"
|
||||
| "make_invoice"
|
||||
| "lookup_invoice";
|
||||
| "make_invoice";
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
@ -161,7 +156,6 @@ async function connect(): Promise<void> {
|
|||
if (!maybePriv) throw new Error("NWC privkey missing");
|
||||
const privkey = maybePriv;
|
||||
const pubkey = getPublicKey(hexToBytes(privkey));
|
||||
const breezSDK = await getBreezSDK();
|
||||
|
||||
pool = new SimplePool();
|
||||
sub = pool.subscribeMany(relays, [
|
||||
|
@ -171,7 +165,7 @@ async function connect(): Promise<void> {
|
|||
},
|
||||
], {
|
||||
onevent: async (evt: NostrEvent) => {
|
||||
await handleEvent(evt, privkey, pubkey, relays, breezSDK);
|
||||
await handleEvent(evt, privkey, pubkey, relays);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -181,7 +175,6 @@ async function handleEvent(
|
|||
privkey: string,
|
||||
ourPubkey: string,
|
||||
relays: string[],
|
||||
breezSDK: BindingLiquidSdk,
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (!verifyEvent(evt)) return;
|
||||
|
@ -231,6 +224,8 @@ async function handleEvent(
|
|||
async function processRpc(
|
||||
req: JsonRpcReq,
|
||||
): Promise<unknown> {
|
||||
const currentWalletState = get(walletState);
|
||||
if (!currentWalletState.open) throw new Error("Wallet not open");
|
||||
console.log("processRpc", req);
|
||||
switch (req.method) {
|
||||
case "pay_invoice": {
|
||||
|
@ -239,102 +234,51 @@ async function processRpc(
|
|||
throw { code: "INTERNAL", message: "missing_invoice" };
|
||||
}
|
||||
|
||||
const breezSDK = await getBreezSDK();
|
||||
const parsed = await breezSDK.parse(destination);
|
||||
|
||||
if (parsed.type !== "bolt11") {
|
||||
throw { code: "INTERNAL", message: "not a bolt11 invoice" };
|
||||
}
|
||||
const paymentHash = (parsed as any).invoice.paymentHash;
|
||||
|
||||
const existing = await nwcDB.paidInvoices.get(paymentHash);
|
||||
if (existing) {
|
||||
throw { code: "OTHER", message: "invoice already paid" };
|
||||
if (await nwcDB.paidInvoices.get(destination)) {
|
||||
throw { code: "OTHER", message: "invoice_already_paid" };
|
||||
}
|
||||
|
||||
const sender = send(destination, 0, parsed);
|
||||
let res = await sender.next();
|
||||
const sender = currentWalletState.wallet.pay(destination, 0);
|
||||
let res;
|
||||
try {
|
||||
res = await sender.next();
|
||||
while (!res.done) {
|
||||
res = await sender.next();
|
||||
}
|
||||
|
||||
if (res.value.error) {
|
||||
throw { code: "PAYMENT_FAILED", message: res.value.error };
|
||||
} catch (err) {
|
||||
throw { code: "PAYMENT_FAILED", message: (err as Error).message };
|
||||
}
|
||||
|
||||
if (res.value.paymentHash) {
|
||||
await nwcDB.paidInvoices.put({
|
||||
paymentHash: res.value.paymentHash,
|
||||
paymentHash: destination,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
}).catch((err) => {
|
||||
console.error("Failed to save paid invoice", err);
|
||||
});
|
||||
}
|
||||
|
||||
await refreshBalances();
|
||||
return {
|
||||
preimage: res.value.preimage,
|
||||
paymentHash: res.value.paymentHash,
|
||||
};
|
||||
return {};
|
||||
// TODO: check if it works without the preimage and paymentHash
|
||||
// return {
|
||||
// preimage: res.value.preimage,
|
||||
// paymentHash: res.value.paymentHash,
|
||||
// };
|
||||
}
|
||||
case "get_balance": {
|
||||
const breezSDK = await getBreezSDK();
|
||||
const info = await breezSDK.getInfo();
|
||||
return {
|
||||
balance: info.walletInfo.balanceSat * 1000 + get(cashuState).balance *
|
||||
1000,
|
||||
balance: currentWalletState.balance * 1000,
|
||||
};
|
||||
}
|
||||
case "list_transactions": {
|
||||
const from_timestamp =
|
||||
(req.params?.from_timestamp as number | undefined) ?? 0;
|
||||
const to_timestamp = (req.params?.to_timestamp as number | undefined) ??
|
||||
undefined;
|
||||
const limit = (req.params?.limit as number | undefined) ?? 20;
|
||||
const offset = (req.params?.offset as number | undefined) ?? undefined;
|
||||
const offset = (req.params?.offset as number | undefined) ?? 0;
|
||||
|
||||
const breezSDK = await getBreezSDK();
|
||||
const liquidPayments = (await breezSDK.listPayments({
|
||||
fromTimestamp: from_timestamp,
|
||||
toTimestamp: to_timestamp,
|
||||
const transactions = await currentWalletState.wallet.listPayments(
|
||||
limit,
|
||||
offset,
|
||||
})) as any[];
|
||||
|
||||
const cashuTxns = await cashuDB.txns.toArray();
|
||||
|
||||
const allTxns = [
|
||||
...liquidPayments.map((p) => ({
|
||||
type: p.paymentType,
|
||||
invoice: p.bolt11,
|
||||
description: p.description,
|
||||
payment_hash: p.paymentHash,
|
||||
preimage: p.paymentPreimage,
|
||||
amount: p.amountSat * 1000,
|
||||
fees_paid: p.feesSat * 1000,
|
||||
created_at: Math.floor(p.paymentTime / 1000),
|
||||
settled_at: p.status === "complete"
|
||||
? Math.floor(p.paymentTime / 1000)
|
||||
: undefined,
|
||||
})),
|
||||
...cashuTxns.map((p) => ({
|
||||
type: p.paymentType,
|
||||
invoice: "",
|
||||
description: "Cashu",
|
||||
payment_hash: p.txId,
|
||||
preimage: "",
|
||||
amount: p.amountSat * 1000,
|
||||
fees_paid: 0,
|
||||
created_at: p.timestamp,
|
||||
settled_at: p.timestamp,
|
||||
})),
|
||||
].sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
return {
|
||||
transactions: allTxns,
|
||||
};
|
||||
);
|
||||
return { transactions };
|
||||
}
|
||||
case "get_info": {
|
||||
const breezSDK = await getBreezSDK();
|
||||
const info = await breezSDK.getInfo();
|
||||
const privkey = get(nwcPrivkey);
|
||||
if (!privkey) throw new Error("missing_privkey");
|
||||
|
||||
|
@ -349,7 +293,6 @@ async function processRpc(
|
|||
"list_transactions",
|
||||
"get_info",
|
||||
"make_invoice",
|
||||
"lookup_invoice",
|
||||
],
|
||||
notifications: [],
|
||||
};
|
||||
|
@ -361,7 +304,9 @@ async function processRpc(
|
|||
throw { code: "INTERNAL", message: "missing_amount" };
|
||||
}
|
||||
|
||||
const npub = await getNpub();
|
||||
const npub = currentWalletState.wallet.npub;
|
||||
if (!npub) throw new Error("Wallet not open");
|
||||
|
||||
const res = await fetch(
|
||||
`/.well-known/lnurl-pay/callback/${npub}?amount=${amountMsat}`,
|
||||
);
|
||||
|
@ -385,35 +330,6 @@ async function processRpc(
|
|||
invoice,
|
||||
};
|
||||
}
|
||||
case "lookup_invoice": {
|
||||
const paymentHash = req.params?.payment_hash as string | undefined;
|
||||
|
||||
if (!paymentHash) {
|
||||
throw { code: "INTERNAL", message: "missing_payment_hash" };
|
||||
}
|
||||
|
||||
const breezSDK = await getBreezSDK();
|
||||
const payments = (await breezSDK.listPayments({})) as any[];
|
||||
const payment = payments.find((p) => p.paymentHash === paymentHash);
|
||||
|
||||
if (!payment) {
|
||||
throw { code: "NOT_FOUND", message: "invoice_not_found" };
|
||||
}
|
||||
|
||||
return {
|
||||
type: payment.paymentType,
|
||||
invoice: payment.bolt11,
|
||||
description: payment.description,
|
||||
payment_hash: payment.paymentHash,
|
||||
preimage: payment.paymentPreimage,
|
||||
amount: payment.amountSat * 1000,
|
||||
fees_paid: payment.feesSat * 1000,
|
||||
created_at: Math.floor(payment.paymentTime / 1000),
|
||||
settled_at: payment.status === "complete"
|
||||
? Math.floor(payment.paymentTime / 1000)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw { code: "NOT_IMPLEMENTED", message: "unknown_method" };
|
||||
}
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
import { getBreezSDK } from "$lib/breez.svelte";
|
||||
import { payBolt11Invoice as payBolt11InvoiceUsingCashu } from "$lib/cashu.svelte";
|
||||
import type {
|
||||
InputType,
|
||||
PreparePayOnchainRequest,
|
||||
PreparePayOnchainResponse,
|
||||
PrepareSendRequest,
|
||||
PrepareSendResponse,
|
||||
} from "@breeztech/breez-sdk-liquid/web";
|
||||
|
||||
export async function* send(
|
||||
destination: string,
|
||||
amount = 0,
|
||||
parsed?: InputType,
|
||||
) {
|
||||
try {
|
||||
yield {
|
||||
status: "Parsing destination…",
|
||||
error: null,
|
||||
};
|
||||
|
||||
const breezSDK = await getBreezSDK();
|
||||
const parsedInvoice: InputType = parsed ??
|
||||
await breezSDK.parse(destination);
|
||||
|
||||
if (parsedInvoice.type === "bolt11") {
|
||||
try {
|
||||
yield {
|
||||
status: "Attempting Cashu payment…",
|
||||
error: null,
|
||||
};
|
||||
await payBolt11InvoiceUsingCashu(destination);
|
||||
return {
|
||||
status: "✅ Payment sent",
|
||||
error: null,
|
||||
preimage: undefined,
|
||||
paymentHash: (parsedInvoice as any).invoice.paymentHash,
|
||||
};
|
||||
} catch (cashuErr) {
|
||||
console.warn("Cashu payment failed, falling back to Breez", cashuErr);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedInvoice.type === "bitcoinAddress") {
|
||||
if (amount <= 0) {
|
||||
return {
|
||||
status: null,
|
||||
error: "Amount required for bitcoin address",
|
||||
};
|
||||
}
|
||||
|
||||
const req: PreparePayOnchainRequest = {
|
||||
amount: { type: "bitcoin", receiverAmountSat: amount },
|
||||
};
|
||||
|
||||
yield {
|
||||
status: "Preparing on-chain payment…",
|
||||
error: null,
|
||||
};
|
||||
const prep: PreparePayOnchainResponse = await breezSDK.preparePayOnchain(
|
||||
req,
|
||||
);
|
||||
|
||||
yield {
|
||||
status: "Broadcasting…",
|
||||
error: null,
|
||||
};
|
||||
await breezSDK.payOnchain({
|
||||
address: parsedInvoice.address.address,
|
||||
prepareResponse: prep,
|
||||
});
|
||||
} else {
|
||||
const req: PrepareSendRequest = { destination };
|
||||
if (amount > 0) {
|
||||
req.amount = {
|
||||
type: "bitcoin",
|
||||
receiverAmountSat: amount,
|
||||
};
|
||||
}
|
||||
|
||||
yield {
|
||||
status: "Preparing…",
|
||||
error: null,
|
||||
};
|
||||
const prep: PrepareSendResponse = await breezSDK.prepareSendPayment(req);
|
||||
yield {
|
||||
status: "Sending…",
|
||||
error: null,
|
||||
};
|
||||
const res = await breezSDK.sendPayment({ prepareResponse: prep });
|
||||
return {
|
||||
status: "✅ Payment sent",
|
||||
error: null,
|
||||
preimage: (res.payment as any)?.paymentPreimage,
|
||||
paymentHash: (res.payment as any)?.paymentHash,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "✅ Payment sent",
|
||||
error: null,
|
||||
preimage: undefined,
|
||||
paymentHash: undefined,
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
console.error("Send failed", e);
|
||||
if (e instanceof Error) {
|
||||
const raw = e.message;
|
||||
const protoMatch = raw.match(/Generic error:.*?\(\"(.+?)\"\)/);
|
||||
return {
|
||||
status: null,
|
||||
error: protoMatch ? protoMatch[1] : raw,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: null,
|
||||
error: "Send failed",
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
import { browser } from "$app/environment";
|
||||
import type { Dexie, Table } from "dexie";
|
||||
import { type Writable, writable } from "svelte/store";
|
||||
|
||||
export function hexToBytes(hex: string): Uint8Array {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function bytesToHex(bytes: Uint8Array): string {
|
||||
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
export interface MetaEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function persistentDbWritable<T, DB extends Dexie>(
|
||||
key: string,
|
||||
initial: T,
|
||||
db: DB & { meta: Table<MetaEntry, string> },
|
||||
): Writable<T> {
|
||||
const store = writable<T>(initial);
|
||||
|
||||
if (browser) {
|
||||
(async () => {
|
||||
try {
|
||||
const entry = await db.meta.get(key);
|
||||
if (entry) {
|
||||
store.set(JSON.parse(entry.value));
|
||||
} else {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw !== null) {
|
||||
store.set(JSON.parse(raw));
|
||||
await db.meta.put({ key, value: raw });
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`NWC store load error for ${key}`, err);
|
||||
}
|
||||
})();
|
||||
|
||||
store.subscribe(async (value) => {
|
||||
try {
|
||||
await db.meta.put({ key, value: JSON.stringify(value) });
|
||||
} catch (err) {
|
||||
console.error(`NWC store persist error for ${key}`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
|
@ -5,37 +5,84 @@ import {
|
|||
decrypt as nip49decrypt,
|
||||
encrypt as nip49encrypt,
|
||||
} from "nostr-tools/nip49";
|
||||
import type { Proof } from "@cashu/cashu-ts";
|
||||
import Dexie from "dexie";
|
||||
import type { Table } from "dexie";
|
||||
import PortalBtcWallet, { type CashuStore } from "portalbtc-lib";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
const SEED_KEY = "seed";
|
||||
let cachedMnemonic = $state<{ mnemonic: string | undefined }>({
|
||||
mnemonic: undefined,
|
||||
});
|
||||
|
||||
export const passwordRequest = $state({
|
||||
pending: false,
|
||||
resolve: undefined as ((password: string) => void) | undefined,
|
||||
reject: undefined as ((error: Error) => void) | undefined,
|
||||
});
|
||||
|
||||
let previouslyInputPassword = $state<{ pass: string | undefined }>({
|
||||
pass: undefined,
|
||||
});
|
||||
|
||||
function requestPassword(): Promise<string> {
|
||||
if (!browser) throw new Error("Password input only available in browser");
|
||||
if (previouslyInputPassword.pass) {
|
||||
return Promise.resolve(previouslyInputPassword.pass);
|
||||
interface MetaEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
passwordRequest.pending = true;
|
||||
passwordRequest.resolve = (pw) => {
|
||||
previouslyInputPassword.pass = pw;
|
||||
resolve(pw);
|
||||
};
|
||||
passwordRequest.reject = reject;
|
||||
interface CashuTxn {
|
||||
txId: string;
|
||||
paymentType: "receive" | "send";
|
||||
amountSat: number;
|
||||
timestamp: number;
|
||||
status: "complete";
|
||||
}
|
||||
|
||||
class CashuDB extends Dexie {
|
||||
proofs!: Table<Proof, string>;
|
||||
meta!: Table<MetaEntry, string>;
|
||||
txns!: Table<CashuTxn, string>;
|
||||
|
||||
constructor() {
|
||||
super("cashu");
|
||||
this.version(1).stores({
|
||||
proofs: "&secret",
|
||||
meta: "&key",
|
||||
});
|
||||
this.version(2).stores({
|
||||
txns: "&txId",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DexieBasedCashuStore implements CashuStore {
|
||||
private readonly db: CashuDB = new CashuDB();
|
||||
|
||||
async getProofs() {
|
||||
return this.db.proofs.toArray();
|
||||
}
|
||||
|
||||
async getTxns() {
|
||||
return this.db.txns.toArray();
|
||||
}
|
||||
|
||||
async getLastRedeemedCashuQuoteTimestamp() {
|
||||
const meta = await this.db.meta.get("lastRedeemedCashuQuoteTimestamp");
|
||||
return meta?.value ? parseInt(meta.value) : 0;
|
||||
}
|
||||
|
||||
async setLastRedeemedCashuQuoteTimestamp(timestamp: number) {
|
||||
await this.db.meta.put({
|
||||
key: "lastRedeemedCashuQuoteTimestamp",
|
||||
value: timestamp.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
async persistProofs(proofs: Proof[]) {
|
||||
await this.db.proofs.clear();
|
||||
if (proofs.length) await this.db.proofs.bulkPut(proofs);
|
||||
}
|
||||
|
||||
async persistTxns(txns: CashuTxn[]) {
|
||||
await this.db.txns.clear();
|
||||
if (txns.length) await this.db.txns.bulkPut(txns);
|
||||
}
|
||||
|
||||
async persistLastRedeemedCashuQuoteTimestamp(timestamp: number) {
|
||||
await this.db.meta.put({
|
||||
key: "lastRedeemedCashuQuoteTimestamp",
|
||||
value: timestamp.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const MNEMONIC_KEY = "seed";
|
||||
|
||||
export function createMnemonic(): string {
|
||||
return generateMnemonic(wordlist, 128);
|
||||
|
@ -65,26 +112,62 @@ export async function decryptMnemonic(
|
|||
return new TextDecoder().decode(decryptedBytes);
|
||||
}
|
||||
|
||||
export function storeEncryptedSeed(encrypted: string) {
|
||||
if (browser) localStorage.setItem(SEED_KEY, encrypted);
|
||||
export function storeEncryptedMnemonic(encrypted: string) {
|
||||
if (browser) localStorage.setItem(MNEMONIC_KEY, encrypted);
|
||||
}
|
||||
|
||||
export function hasSeed(): boolean {
|
||||
export function hasMnemonic(): boolean {
|
||||
if (!browser) return false;
|
||||
return localStorage.getItem(SEED_KEY) !== null;
|
||||
return localStorage.getItem(MNEMONIC_KEY) !== null;
|
||||
}
|
||||
|
||||
export async function getMnemonic(): Promise<string> {
|
||||
if (cachedMnemonic.mnemonic) return cachedMnemonic.mnemonic;
|
||||
if (!browser) throw new Error("Not in browser");
|
||||
const enc = localStorage.getItem(SEED_KEY);
|
||||
if (!enc) throw new Error("Seed not initialised");
|
||||
const password = await requestPassword();
|
||||
const mnemonic = await decryptMnemonic(enc, password);
|
||||
cachedMnemonic.mnemonic = mnemonic;
|
||||
return mnemonic;
|
||||
export const walletState = writable<
|
||||
| {
|
||||
open: false;
|
||||
wallet: undefined;
|
||||
mnemonic: undefined;
|
||||
balance: number;
|
||||
pendingBalance: number;
|
||||
cashuBalance: number;
|
||||
}
|
||||
| {
|
||||
open: true;
|
||||
mnemonic: string;
|
||||
wallet: PortalBtcWallet;
|
||||
balance: number;
|
||||
pendingBalance: number;
|
||||
cashuBalance: number;
|
||||
}
|
||||
>({
|
||||
open: false,
|
||||
wallet: undefined,
|
||||
mnemonic: undefined,
|
||||
balance: 0,
|
||||
pendingBalance: 0,
|
||||
cashuBalance: 0,
|
||||
});
|
||||
|
||||
export function cacheMnemonic(mnemonic: string) {
|
||||
cachedMnemonic.mnemonic = mnemonic;
|
||||
export async function openWallet(mnemonic: string) {
|
||||
const wallet = await PortalBtcWallet.create(
|
||||
mnemonic,
|
||||
new DexieBasedCashuStore(),
|
||||
false,
|
||||
"mainnet",
|
||||
);
|
||||
wallet.addEventListener("balanceUpdated", () => {
|
||||
walletState.update((state) => ({
|
||||
...state,
|
||||
balance: wallet.balance,
|
||||
pendingBalance: wallet.pendingBalance,
|
||||
cashuBalance: wallet.cashuBalance,
|
||||
}));
|
||||
});
|
||||
walletState.set({
|
||||
open: true,
|
||||
mnemonic,
|
||||
wallet,
|
||||
balance: wallet.balance,
|
||||
pendingBalance: wallet.pendingBalance,
|
||||
cashuBalance: wallet.cashuBalance,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts">
|
||||
import "$lib/style.css";
|
||||
import "iconify-icon";
|
||||
import { hasSeed, passwordRequest } from "$lib/wallet.svelte";
|
||||
import { hasMnemonic, walletState } from "$lib/wallet.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import PasswordDialog from "$lib/components/PasswordDialog.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { page } from "$app/state";
|
||||
import { start as startNwc } from "$lib/nwc.svelte";
|
||||
import SplashScreen from "$lib/components/SplashScreen.svelte";
|
||||
import ErrorDialog from "$lib/components/ErrorDialog.svelte";
|
||||
|
@ -16,7 +16,7 @@
|
|||
stack?: string;
|
||||
};
|
||||
|
||||
let passwordDialogOpen = $state(false);
|
||||
let { children } = $props();
|
||||
let showSplash = $state(true);
|
||||
let currentError = $state<AppError | null>(null);
|
||||
let showInstallPrompt = $state(false);
|
||||
|
@ -33,32 +33,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
function handleUnlockAttempt(pw: string) {
|
||||
if (passwordRequest.resolve) {
|
||||
passwordRequest.resolve(pw);
|
||||
passwordRequest.resolve = undefined;
|
||||
passwordRequest.reject = undefined;
|
||||
passwordRequest.pending = false;
|
||||
startNwc();
|
||||
}
|
||||
passwordDialogOpen = false;
|
||||
}
|
||||
|
||||
function goToSettings() {
|
||||
goto("/settings");
|
||||
}
|
||||
|
||||
const { children } = $props();
|
||||
|
||||
$effect(() => {
|
||||
if (passwordRequest.pending && !passwordDialogOpen) {
|
||||
passwordDialogOpen = true;
|
||||
}
|
||||
});
|
||||
|
||||
const isOnSetup = $derived($page.route.id?.startsWith("/setup"));
|
||||
const isOnSettings = $derived($page.route.id?.startsWith("/settings"));
|
||||
const showSettingsButton = $derived(!isOnSetup && !isOnSettings && hasSeed());
|
||||
const isOnSetup = $derived(page.route.id?.startsWith("/setup"));
|
||||
const isOnSettings = $derived(page.route.id?.startsWith("/settings"));
|
||||
const showSettingsButton = $derived(
|
||||
!isOnSetup && !isOnSettings && hasMnemonic()
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
|
@ -96,7 +79,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
if (!hasSeed() && !isOnSetup) {
|
||||
if (!hasMnemonic() && !isOnSetup) {
|
||||
goto("/setup");
|
||||
}
|
||||
});
|
||||
|
@ -121,12 +104,14 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isOnSetup || !passwordRequest.pending}
|
||||
{#if isOnSetup || $walletState.open}
|
||||
{@render children()}
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<PasswordDialog bind:open={passwordDialogOpen} unlock={handleUnlockAttempt} />
|
||||
{#if !$walletState.open && !isOnSetup}
|
||||
<PasswordDialog onunlock={() => startNwc()} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<ErrorDialog error={currentError} onclose={() => (currentError = null)} />
|
||||
|
|
|
@ -1,24 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { BindingLiquidSdk } from "@breeztech/breez-sdk-liquid/web";
|
||||
import { balances, getBreezSDK, initBalances } from "$lib/breez.svelte";
|
||||
import BalanceDisplay from "$lib/components/BalanceDisplay.svelte";
|
||||
import PaymentHistory from "$lib/components/PaymentHistory.svelte";
|
||||
import ReceiveDialog from "$lib/components/ReceiveDialog.svelte";
|
||||
import SendDialog from "$lib/components/SendDialog.svelte";
|
||||
import Button from "$lib/components/Button.svelte";
|
||||
import { refreshBalances } from "$lib/breez.svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let breezSDK = $state<BindingLiquidSdk | undefined>(undefined);
|
||||
|
||||
let receiveDialogOpen = $state(false);
|
||||
let sendDialogOpen = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
initBalances();
|
||||
breezSDK = await getBreezSDK();
|
||||
});
|
||||
|
||||
function openReceiveDialog() {
|
||||
receiveDialogOpen = true;
|
||||
}
|
||||
|
@ -26,20 +15,13 @@
|
|||
function openSendDialog() {
|
||||
sendDialogOpen = true;
|
||||
}
|
||||
|
||||
function onPaymentSent() {
|
||||
refreshBalances();
|
||||
}
|
||||
</script>
|
||||
|
||||
<ReceiveDialog bind:open={receiveDialogOpen} />
|
||||
<SendDialog bind:open={sendDialogOpen} onsent={onPaymentSent} />
|
||||
<SendDialog bind:open={sendDialogOpen} />
|
||||
|
||||
<div class="container">
|
||||
<BalanceDisplay
|
||||
balance={balances.balance}
|
||||
pending={balances.pendingReceive - balances.pendingSend}
|
||||
/>
|
||||
<BalanceDisplay />
|
||||
<div class="retro-card send-receive-buttons">
|
||||
<Button variant="primary" onclick={openReceiveDialog}>Receive</Button>
|
||||
<Button variant="danger" onclick={openSendDialog}>Send</Button>
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
import {
|
||||
createMnemonic,
|
||||
encryptMnemonic,
|
||||
storeEncryptedSeed,
|
||||
cacheMnemonic,
|
||||
storeEncryptedMnemonic,
|
||||
isValidMnemonic,
|
||||
openWallet,
|
||||
} from "$lib/wallet.svelte";
|
||||
import { start as startNwc } from "$lib/nwc.svelte";
|
||||
|
||||
|
@ -77,8 +77,8 @@
|
|||
return;
|
||||
}
|
||||
const encrypted = await encryptMnemonic(userMnemonic, password);
|
||||
storeEncryptedSeed(encrypted);
|
||||
cacheMnemonic(userMnemonic);
|
||||
storeEncryptedMnemonic(encrypted);
|
||||
await openWallet(userMnemonic);
|
||||
|
||||
startNwc();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue