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 ."
|
"lint": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@breeztech/breez-sdk-liquid": "^0.9.2-rc2",
|
|
||||||
"@cashu/cashu-ts": "^2.5.2",
|
|
||||||
"@libsql/client": "^0.15.9",
|
"@libsql/client": "^0.15.9",
|
||||||
"@scure/base": "^1.2.6",
|
"@scure/base": "^1.2.6",
|
||||||
"@scure/bip39": "^1.6.0",
|
"@scure/bip39": "^1.6.0",
|
||||||
"@sveltejs/adapter-auto": "^6.0.1",
|
"@sveltejs/adapter-auto": "^6.0.1",
|
||||||
"@sveltejs/adapter-node": "^5.2.12",
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
"@types/qrcode": "^1.5.5",
|
|
||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.11",
|
||||||
"iconify-icon": "^3.0.0",
|
"iconify-icon": "^3.0.0",
|
||||||
"nostr-tools": "^2.15.0",
|
"nostr-tools": "^2.15.0",
|
||||||
|
"portalbtc-lib": "git+ssh://git@git.arx-ccn.com:222/Arx/PortalBTCLib.git#2d858727b05f9d66e4f028c086d93cebf769f3b8",
|
||||||
"qrcode": "^1.5.4"
|
"qrcode": "^1.5.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@sveltejs/kit": "^2.16.0",
|
"@sveltejs/kit": "^2.16.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"prettier": "^3.4.2",
|
"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">
|
<script lang="ts">
|
||||||
import { satsComma } from "$lib";
|
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 } =
|
let loading = $derived(!$walletState.open);
|
||||||
$props();
|
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 formattedPending = $derived(satsComma(pending));
|
||||||
const formattedLightning = $derived(satsComma(balance));
|
const formattedLightning = $derived(satsComma(balance));
|
||||||
const formattedCashu = $derived(satsComma($cashuState.balance));
|
const formattedCashu = $derived(satsComma(cashuBalance));
|
||||||
const cashuThreshold = $derived(satsComma($cashuState.meltThreshold));
|
|
||||||
|
|
||||||
let showSplit = $state(false);
|
let showSplit = $state(false);
|
||||||
let containerEl: HTMLButtonElement;
|
let containerEl: HTMLButtonElement;
|
||||||
|
@ -38,24 +44,48 @@
|
||||||
aria-label="Toggle balance split"
|
aria-label="Toggle balance split"
|
||||||
>
|
>
|
||||||
<div class="balance-label">Total Balance</div>
|
<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-label">Pending</div>
|
||||||
<div class="pending-value">{formattedPending} sats</div>
|
<div class="pending-value">
|
||||||
|
{#if loading}
|
||||||
|
<LoadingIndicator />
|
||||||
|
{:else}
|
||||||
|
{formattedPending} sats
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{#if showSplit}
|
{#if showSplit}
|
||||||
<div class="popover">
|
<div class="popover">
|
||||||
<div class="split-row lightning">
|
<div class="split-row lightning">
|
||||||
<iconify-icon icon="icon-park-twotone:lightning" width="32" height="32"
|
<iconify-icon icon="icon-park-twotone:lightning" width="32" height="32"
|
||||||
></iconify-icon>
|
></iconify-icon>
|
||||||
<span class="label">Lightning</span>
|
<span class="label">Lightning</span>
|
||||||
<span class="amount">{formattedLightning} sats</span>
|
<span class="amount">
|
||||||
|
{#if loading}
|
||||||
|
<LoadingIndicator />
|
||||||
|
{:else}
|
||||||
|
{formattedLightning} sats
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="split-row cashu">
|
<div class="split-row cashu">
|
||||||
<iconify-icon icon="tdesign:nut-filled" width="32" height="32"
|
<iconify-icon icon="tdesign:nut-filled" width="32" height="32"
|
||||||
></iconify-icon>
|
></iconify-icon>
|
||||||
<span class="label">Cashu</span>
|
<span class="label">Cashu</span>
|
||||||
<span class="amount">{formattedCashu} sats</span>
|
<span class="amount">
|
||||||
|
{#if loading}
|
||||||
|
<LoadingIndicator />
|
||||||
|
{:else}
|
||||||
|
{formattedCashu} sats
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="threshold">Auto-melt at {cashuThreshold} sats</div>
|
<div class="threshold">Auto-melt at 2000 sats</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from "$lib/components/Button.svelte";
|
import Button from "$lib/components/Button.svelte";
|
||||||
import { redeemToken } from "$lib/cashu.svelte";
|
import { walletState } from "$lib/wallet.svelte";
|
||||||
|
|
||||||
let tokenInput = $state("");
|
let tokenInput = $state("");
|
||||||
|
|
||||||
function redeem() {
|
function redeem() {
|
||||||
redeemToken(tokenInput).catch((e) => alert(e.message));
|
if (!$walletState.open) return;
|
||||||
|
$walletState.wallet.redeemToken(tokenInput).catch((e) => alert(e.message));
|
||||||
tokenInput = "";
|
tokenInput = "";
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,26 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from "./Button.svelte";
|
import Button from "./Button.svelte";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { getMnemonic } from "$lib/wallet.svelte";
|
import { walletState } from "$lib/wallet.svelte";
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
let seedPhrase = $state("");
|
|
||||||
let showSeedPhrase = $state(false);
|
let showSeedPhrase = $state(false);
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
seedPhrase = await getMnemonic();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to get mnemonic:", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function toggleSeedPhrase() {
|
function toggleSeedPhrase() {
|
||||||
showSeedPhrase = !showSeedPhrase;
|
showSeedPhrase = !showSeedPhrase;
|
||||||
}
|
}
|
||||||
|
|
||||||
function copySeedPhrase() {
|
function copySeedPhrase() {
|
||||||
navigator.clipboard.writeText(seedPhrase);
|
navigator.clipboard.writeText($walletState.mnemonic!);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetWebsite() {
|
function resetWebsite() {
|
||||||
|
@ -57,7 +47,7 @@
|
||||||
|
|
||||||
{#if showSeedPhrase}
|
{#if showSeedPhrase}
|
||||||
<div class="seed-phrase">
|
<div class="seed-phrase">
|
||||||
{seedPhrase}
|
{$walletState.mnemonic!}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Button from "./Button.svelte";
|
import Button from "./Button.svelte";
|
||||||
import { BASE_DOMAIN } from "$lib/config";
|
import { BASE_DOMAIN } from "$lib/config";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { getNpub } from "$lib/cashu.svelte";
|
import { walletState } from "$lib/wallet.svelte";
|
||||||
|
|
||||||
let username = $state("");
|
let username = $state("");
|
||||||
let existingUsername = $state("");
|
let existingUsername = $state("");
|
||||||
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
async function checkExistingUsername() {
|
async function checkExistingUsername() {
|
||||||
try {
|
try {
|
||||||
const userNpub = await getNpub();
|
if (!$walletState.open) return;
|
||||||
|
const userNpub = $walletState.wallet.npub;
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/check-username?npub=${encodeURIComponent(userNpub)}`
|
`/api/check-username?npub=${encodeURIComponent(userNpub)}`
|
||||||
);
|
);
|
||||||
|
@ -45,7 +46,8 @@
|
||||||
registrationStatus = "";
|
registrationStatus = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userNpub = await getNpub();
|
if (!$walletState.open) return;
|
||||||
|
const userNpub = $walletState.wallet.npub;
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/.well-known/lnurlp/${encodeURIComponent(username)}`,
|
`/.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">
|
<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 { browser } from "$app/environment";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
let {
|
let { onunlock }: { onunlock: () => void } = $props();
|
||||||
open = $bindable(),
|
|
||||||
unlock,
|
|
||||||
}: { open: boolean; unlock: (pw: string) => void } = $props();
|
|
||||||
|
|
||||||
let dialogEl: HTMLDialogElement | null = $state(null);
|
let dialogEl: HTMLDialogElement | null = $state(null);
|
||||||
let password = $state("");
|
let password = $state("");
|
||||||
let error = $state("");
|
let error = $state("");
|
||||||
let isValidating = $state(false);
|
let isValidating = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
onMount(() => {
|
||||||
if (open) dialogEl?.showModal();
|
dialogEl?.showModal();
|
||||||
else dialogEl?.close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function closeDialog() {
|
|
||||||
open = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function attemptUnlock() {
|
async function attemptUnlock() {
|
||||||
if (!password.trim()) return;
|
if (!password.trim()) return;
|
||||||
|
|
||||||
|
@ -29,55 +27,52 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!browser) throw new Error("Not in browser");
|
if (!browser) throw new Error("Not in browser");
|
||||||
const encryptedSeed = localStorage.getItem("seed");
|
if (!hasMnemonic()) throw new Error("No encrypted seed found");
|
||||||
if (!encryptedSeed) throw new Error("No encrypted seed found");
|
const encryptedMnemonic = localStorage.getItem(MNEMONIC_KEY)!;
|
||||||
await decryptMnemonic(encryptedSeed, password);
|
const mnemonic = await decryptMnemonic(encryptedMnemonic, password);
|
||||||
unlock(password);
|
await openWallet(mnemonic);
|
||||||
password = "";
|
onunlock();
|
||||||
error = "";
|
dialogEl?.close();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = "Incorrect password";
|
error = "Incorrect password";
|
||||||
console.error("Password validation failed:", err);
|
console.error("Password validation failed:", err);
|
||||||
} finally {
|
|
||||||
isValidating = false;
|
isValidating = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if open}
|
<dialog bind:this={dialogEl}>
|
||||||
<dialog bind:this={dialogEl} onclose={closeDialog}>
|
<h2>Unlock Wallet</h2>
|
||||||
<h2>Unlock Wallet</h2>
|
<p>Enter your wallet password to decrypt your seed.</p>
|
||||||
<p>Enter your wallet password to decrypt your seed.</p>
|
{#if error}
|
||||||
{#if error}
|
<p
|
||||||
<p
|
style="color: var(--error-color); font-size: 0.7rem; margin-bottom: 1rem;"
|
||||||
style="color: var(--error-color); font-size: 0.7rem; margin-bottom: 1rem;"
|
>
|
||||||
>
|
{error}
|
||||||
{error}
|
</p>
|
||||||
</p>
|
{/if}
|
||||||
{/if}
|
<input
|
||||||
<input
|
type="password"
|
||||||
type="password"
|
class="retro-input"
|
||||||
class="retro-input"
|
bind:value={password}
|
||||||
bind:value={password}
|
placeholder="Password"
|
||||||
placeholder="Password"
|
disabled={isValidating}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === "Enter" && !isValidating) {
|
||||||
|
attemptUnlock();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style="margin-top:1rem;text-align:center;">
|
||||||
|
<button
|
||||||
|
class="retro-btn primary"
|
||||||
disabled={isValidating}
|
disabled={isValidating}
|
||||||
onkeydown={(e) => {
|
onclick={() => {
|
||||||
if (e.key === "Enter" && !isValidating) {
|
attemptUnlock();
|
||||||
attemptUnlock();
|
}}>{isValidating ? "Validating..." : "Unlock"}</button
|
||||||
}
|
>
|
||||||
}}
|
</div>
|
||||||
/>
|
</dialog>
|
||||||
<div style="margin-top:1rem;text-align:center;">
|
|
||||||
<button
|
|
||||||
class="retro-btn primary"
|
|
||||||
disabled={isValidating}
|
|
||||||
onclick={() => {
|
|
||||||
attemptUnlock();
|
|
||||||
}}>{isValidating ? "Validating..." : "Unlock"}</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
dialog {
|
dialog {
|
||||||
|
|
|
@ -1,42 +1,27 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { balances, getBreezSDK } from "$lib/breez.svelte";
|
|
||||||
import PaymentHistoryItem from "$lib/components/PaymentHistoryItem.svelte";
|
import PaymentHistoryItem from "$lib/components/PaymentHistoryItem.svelte";
|
||||||
import type {
|
import LoadingIndicator from "$lib/components/LoadingIndicator.svelte";
|
||||||
BindingLiquidSdk,
|
import { walletState } from "$lib/wallet.svelte";
|
||||||
Payment,
|
|
||||||
} from "@breeztech/breez-sdk-liquid/web";
|
|
||||||
import { cashuTxns } from "$lib/cashu.svelte";
|
|
||||||
|
|
||||||
let payments = $state<Payment[]>([]);
|
let payments = $derived(
|
||||||
|
$walletState.open
|
||||||
const combinedPayments = $derived(
|
? $walletState.wallet?.listPayments(100, 0)
|
||||||
[...payments, ...$cashuTxns].sort((a, b) => b.timestamp - a.timestamp)
|
: 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>
|
</script>
|
||||||
|
|
||||||
<div class="payment-history">
|
<div class="payment-history">
|
||||||
{#if combinedPayments.length === 0}
|
{#await payments}
|
||||||
<p class="empty">No payments yet.</p>
|
<LoadingIndicator />
|
||||||
{:else}
|
{:then payments}
|
||||||
{#each combinedPayments as payment (payment.txId)}
|
{#if payments.length === 0}
|
||||||
<PaymentHistoryItem {payment} />
|
<p class="empty">No payments yet.</p>
|
||||||
{/each}
|
{:else}
|
||||||
{/if}
|
{#each payments as payment (payment.txId)}
|
||||||
|
<PaymentHistoryItem {payment} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Payment } from "@breeztech/breez-sdk-liquid/web";
|
import type { Payment } from "@breeztech/breez-sdk-liquid/web";
|
||||||
import { satsComma } from "$lib";
|
import { satsComma } from "$lib";
|
||||||
import { getBreezSDK } from "$lib/breez.svelte";
|
|
||||||
import Button from "$lib/components/Button.svelte";
|
import Button from "$lib/components/Button.svelte";
|
||||||
|
import { walletState } from "$lib/wallet.svelte";
|
||||||
|
|
||||||
interface CashuPayment {
|
interface CashuPayment {
|
||||||
txId: string;
|
txId: string;
|
||||||
|
@ -29,60 +29,15 @@
|
||||||
let isRefunding = $state(false);
|
let isRefunding = $state(false);
|
||||||
|
|
||||||
async function refundPayment() {
|
async function refundPayment() {
|
||||||
|
if (!$walletState.open) return;
|
||||||
if (isRefunding) return;
|
if (isRefunding) return;
|
||||||
isRefunding = true;
|
isRefunding = true;
|
||||||
const breezSDK = await getBreezSDK();
|
if (payment.paymentType !== "send") return;
|
||||||
const prepareAddr = await breezSDK.prepareReceivePayment({
|
if (payment.txId?.startsWith("cashu-")) return;
|
||||||
paymentMethod: "bitcoinAddress",
|
|
||||||
});
|
|
||||||
const receiveRes = await breezSDK.receivePayment({
|
|
||||||
prepareResponse: prepareAddr,
|
|
||||||
});
|
|
||||||
const refundAddress = receiveRes.destination as string;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const refundables = await breezSDK.listRefundables();
|
await $walletState.wallet.refundPayment(payment);
|
||||||
let swapAddress: string | undefined;
|
} catch (e) {
|
||||||
if (refundables.length === 1) {
|
console.error(e);
|
||||||
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}`);
|
|
||||||
} finally {
|
} finally {
|
||||||
isRefunding = false;
|
isRefunding = false;
|
||||||
}
|
}
|
||||||
|
@ -105,7 +60,7 @@
|
||||||
</a>
|
</a>
|
||||||
{:else if (payment as Payment).details.type === "lightning"}
|
{:else if (payment as Payment).details.type === "lightning"}
|
||||||
<a
|
<a
|
||||||
href="https://liquid.network/tx/{payment.details.claimTxId}"
|
href="https://liquid.network/tx/{payment.claimTxId}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
...{payment.txId?.slice(-16)}
|
...{payment.txId?.slice(-16)}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from "$lib/components/Button.svelte";
|
|
||||||
import Tabs from "$lib/components/Tabs.svelte";
|
import Tabs from "$lib/components/Tabs.svelte";
|
||||||
import CashuTab from "$lib/components/CashuTab.svelte";
|
import CashuTab from "$lib/components/CashuTab.svelte";
|
||||||
import ReceiveTab from "$lib/components/ReceiveTab.svelte";
|
import ReceiveTab from "$lib/components/ReceiveTab.svelte";
|
||||||
|
import { walletState } from "$lib/wallet.svelte";
|
||||||
|
|
||||||
let { open = $bindable() }: { open: boolean } = $props();
|
let { open = $bindable() }: { open: boolean } = $props();
|
||||||
|
|
||||||
|
@ -23,27 +23,29 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h3>Receive</h3>
|
<h3>Receive</h3>
|
||||||
|
|
||||||
<Tabs labels={["Bitcoin", "Lightning", "Liquid", "Cashu"]}>
|
{#if $walletState.open}
|
||||||
{#snippet children(idx)}
|
<Tabs labels={["Bitcoin", "Lightning", "Liquid", "Cashu"]}>
|
||||||
{#if idx === 0}
|
{#snippet children(idx)}
|
||||||
<ReceiveTab type="bitcoin" alt="Bitcoin Address" />
|
{#if idx === 0}
|
||||||
{:else if idx === 1}
|
<ReceiveTab type="bitcoin" />
|
||||||
<Tabs labels={["Address", "BOLT12"]}>
|
{:else if idx === 1}
|
||||||
{#snippet children(idx)}
|
<Tabs labels={["Address", "BOLT12"]}>
|
||||||
{#if idx === 0}
|
{#snippet children(idx)}
|
||||||
<ReceiveTab type="lightning" alt="Lightning Address" />
|
{#if idx === 0}
|
||||||
{:else}
|
<ReceiveTab type="lightning" />
|
||||||
<ReceiveTab type="bolt12" alt="Lightning Address" />
|
{:else}
|
||||||
{/if}
|
<ReceiveTab type="bolt12" />
|
||||||
{/snippet}
|
{/if}
|
||||||
</Tabs>
|
{/snippet}
|
||||||
{:else if idx === 2}
|
</Tabs>
|
||||||
<ReceiveTab type="liquid" alt="Liquid Address" />
|
{:else if idx === 2}
|
||||||
{:else}
|
<ReceiveTab type="liquid" />
|
||||||
<CashuTab />
|
{:else}
|
||||||
{/if}
|
<CashuTab />
|
||||||
{/snippet}
|
{/if}
|
||||||
</Tabs>
|
{/snippet}
|
||||||
|
</Tabs>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
|
|
@ -1,54 +1,47 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getBreezSDK } from "$lib/breez.svelte";
|
import { walletState } from "$lib/wallet.svelte";
|
||||||
import { getCashuAddress } from "$lib/cashu.svelte";
|
|
||||||
import Button from "$lib/components/Button.svelte";
|
import Button from "$lib/components/Button.svelte";
|
||||||
import QRCode from "qrcode";
|
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("");
|
||||||
type === "bitcoin"
|
const loading = $derived(paymentText === "");
|
||||||
? "bitcoinAddress"
|
|
||||||
: type === "bolt12"
|
|
||||||
? "bolt12Offer"
|
|
||||||
: type === "lightning"
|
|
||||||
? "bolt11Invoice"
|
|
||||||
: "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(
|
const qr = $derived(
|
||||||
paymentText.then((text) =>
|
QRCode.toDataURL(`${type}:${paymentText}`, {
|
||||||
QRCode.toDataURL(`${type}:${text}`, {
|
errorCorrectionLevel: "H",
|
||||||
errorCorrectionLevel: "H",
|
})
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!$walletState.open) return;
|
||||||
|
const paymentMethod =
|
||||||
|
type === "bitcoin"
|
||||||
|
? "bitcoinAddress"
|
||||||
|
: type === "bolt12"
|
||||||
|
? "bolt12Offer"
|
||||||
|
: type === "lightning"
|
||||||
|
? "bolt11Invoice"
|
||||||
|
: "liquidAddress";
|
||||||
|
|
||||||
|
if (paymentMethod === "bolt11Invoice") {
|
||||||
|
paymentText = $walletState.wallet.lightningAddress;
|
||||||
|
} else {
|
||||||
|
paymentText = await $walletState.wallet.generateAddress(paymentMethod);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await qr}
|
{#if !loading}
|
||||||
<div class="spinner"></div>
|
{#await qr}
|
||||||
{:then qrDataUrl}
|
<LoadingIndicator size={120} />
|
||||||
<img class="qr" src={qrDataUrl} alt="${alt}" />
|
{:then qrDataUrl}
|
||||||
{/await}
|
<img class="qr" src={qrDataUrl} alt={paymentText} />
|
||||||
|
{/await}
|
||||||
|
|
||||||
{#await paymentText then paymentText}
|
|
||||||
<pre class="addr">{paymentText}</pre>
|
<pre class="addr">{paymentText}</pre>
|
||||||
<Button
|
<Button
|
||||||
class="copy-btn"
|
class="copy-btn"
|
||||||
|
@ -56,7 +49,9 @@
|
||||||
>
|
>
|
||||||
Copy
|
Copy
|
||||||
</Button>
|
</Button>
|
||||||
{/await}
|
{:else}
|
||||||
|
<LoadingIndicator />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
img.qr {
|
img.qr {
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getBreezSDK } from "$lib/breez.svelte";
|
|
||||||
import { send as sendPayment } from "$lib/payments";
|
|
||||||
import Button from "$lib/components/Button.svelte";
|
import Button from "$lib/components/Button.svelte";
|
||||||
import type { InputType } from "@breeztech/breez-sdk-liquid/web";
|
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 } =
|
let { open = $bindable() }: { open: boolean } = $props();
|
||||||
$props();
|
|
||||||
|
|
||||||
let destination = $state("");
|
let destination = $state("");
|
||||||
|
|
||||||
|
@ -21,8 +20,9 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const breezSDK = await getBreezSDK();
|
if (!$walletState.open) return;
|
||||||
const parsed: InputType = await breezSDK.parse(destination);
|
const parsed: InputType =
|
||||||
|
await $walletState.wallet.parsePayment(destination);
|
||||||
if (
|
if (
|
||||||
parsed.type === "bolt11" &&
|
parsed.type === "bolt11" &&
|
||||||
parsed.invoice.amountMsat !== undefined &&
|
parsed.invoice.amountMsat !== undefined &&
|
||||||
|
@ -42,16 +42,51 @@
|
||||||
let dialogEl: HTMLDialogElement;
|
let dialogEl: HTMLDialogElement;
|
||||||
|
|
||||||
async function handleSend() {
|
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 {
|
for await (const {
|
||||||
status: sendStatus,
|
status: sendStatus,
|
||||||
error: sendError,
|
error: sendError,
|
||||||
} of sendGenerator) {
|
} of sendGenerator) {
|
||||||
if (sendError) return alert(sendError);
|
if (sendError && sendStatus !== PaymentStatus.CashuPaymentFailed)
|
||||||
status = sendStatus || "";
|
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);
|
setTimeout(closeDialog, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { MAX_SATS_RECEIVE, MIN_SATS_RECEIVE } from "$lib/config";
|
import { MAX_SATS_RECEIVE, MIN_SATS_RECEIVE } from "$lib/config";
|
||||||
import { getDb } from "$lib/database";
|
import { getDb } from "$lib/database";
|
||||||
import { bech32 } from "@scure/base";
|
import { bech32 } from "@scure/base";
|
||||||
|
import type { Dexie, Table } from "dexie";
|
||||||
|
import { type Writable, writable } from "svelte/store";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
export function satsComma(sats: number) {
|
export function satsComma(sats: number) {
|
||||||
const chars = sats.toString().split("").reverse();
|
const chars = sats.toString().split("").reverse();
|
||||||
|
@ -74,3 +77,60 @@ export async function getUser(username: string) {
|
||||||
);
|
);
|
||||||
return result.rows[0];
|
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,
|
generateSecretKey,
|
||||||
getPublicKey,
|
getPublicKey,
|
||||||
nip04,
|
nip04,
|
||||||
nip19,
|
|
||||||
SimplePool,
|
SimplePool,
|
||||||
verifyEvent,
|
verifyEvent,
|
||||||
} from "nostr-tools";
|
} from "nostr-tools";
|
||||||
import type { Event as NostrEvent } 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 { browser } from "$app/environment";
|
||||||
import { getBreezSDK, refreshBalances } from "$lib/breez.svelte";
|
|
||||||
import type { BindingLiquidSdk } from "@breeztech/breez-sdk-liquid/web";
|
|
||||||
import {
|
import {
|
||||||
bytesToHex,
|
bytesToHex,
|
||||||
hexToBytes,
|
hexToBytes,
|
||||||
type MetaEntry,
|
type MetaEntry,
|
||||||
persistentDbWritable,
|
persistentDbWritable,
|
||||||
} from "./utils";
|
} from "$lib";
|
||||||
import { cashuDB, cashuState, getNpub } from "./cashu.svelte";
|
import { walletState } from "$lib/wallet.svelte";
|
||||||
import { send } from "./payments";
|
|
||||||
|
|
||||||
interface PaidInvoice {
|
interface PaidInvoice {
|
||||||
paymentHash: string;
|
paymentHash: string;
|
||||||
|
@ -81,8 +77,7 @@ interface JsonRpcReq {
|
||||||
| "get_balance"
|
| "get_balance"
|
||||||
| "list_transactions"
|
| "list_transactions"
|
||||||
| "get_info"
|
| "get_info"
|
||||||
| "make_invoice"
|
| "make_invoice";
|
||||||
| "lookup_invoice";
|
|
||||||
params: Record<string, unknown>;
|
params: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +156,6 @@ async function connect(): Promise<void> {
|
||||||
if (!maybePriv) throw new Error("NWC privkey missing");
|
if (!maybePriv) throw new Error("NWC privkey missing");
|
||||||
const privkey = maybePriv;
|
const privkey = maybePriv;
|
||||||
const pubkey = getPublicKey(hexToBytes(privkey));
|
const pubkey = getPublicKey(hexToBytes(privkey));
|
||||||
const breezSDK = await getBreezSDK();
|
|
||||||
|
|
||||||
pool = new SimplePool();
|
pool = new SimplePool();
|
||||||
sub = pool.subscribeMany(relays, [
|
sub = pool.subscribeMany(relays, [
|
||||||
|
@ -171,7 +165,7 @@ async function connect(): Promise<void> {
|
||||||
},
|
},
|
||||||
], {
|
], {
|
||||||
onevent: async (evt: NostrEvent) => {
|
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,
|
privkey: string,
|
||||||
ourPubkey: string,
|
ourPubkey: string,
|
||||||
relays: string[],
|
relays: string[],
|
||||||
breezSDK: BindingLiquidSdk,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!verifyEvent(evt)) return;
|
if (!verifyEvent(evt)) return;
|
||||||
|
@ -231,6 +224,8 @@ async function handleEvent(
|
||||||
async function processRpc(
|
async function processRpc(
|
||||||
req: JsonRpcReq,
|
req: JsonRpcReq,
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
|
const currentWalletState = get(walletState);
|
||||||
|
if (!currentWalletState.open) throw new Error("Wallet not open");
|
||||||
console.log("processRpc", req);
|
console.log("processRpc", req);
|
||||||
switch (req.method) {
|
switch (req.method) {
|
||||||
case "pay_invoice": {
|
case "pay_invoice": {
|
||||||
|
@ -239,102 +234,51 @@ async function processRpc(
|
||||||
throw { code: "INTERNAL", message: "missing_invoice" };
|
throw { code: "INTERNAL", message: "missing_invoice" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const breezSDK = await getBreezSDK();
|
if (await nwcDB.paidInvoices.get(destination)) {
|
||||||
const parsed = await breezSDK.parse(destination);
|
throw { code: "OTHER", message: "invoice_already_paid" };
|
||||||
|
|
||||||
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" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sender = send(destination, 0, parsed);
|
const sender = currentWalletState.wallet.pay(destination, 0);
|
||||||
let res = await sender.next();
|
let res;
|
||||||
while (!res.done) {
|
try {
|
||||||
res = await sender.next();
|
res = await sender.next();
|
||||||
|
while (!res.done) {
|
||||||
|
res = await sender.next();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw { code: "PAYMENT_FAILED", message: (err as Error).message };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.value.error) {
|
await nwcDB.paidInvoices.put({
|
||||||
throw { code: "PAYMENT_FAILED", message: res.value.error };
|
paymentHash: destination,
|
||||||
}
|
timestamp: Math.floor(Date.now() / 1000),
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error("Failed to save paid invoice", err);
|
||||||
|
});
|
||||||
|
|
||||||
if (res.value.paymentHash) {
|
return {};
|
||||||
await nwcDB.paidInvoices.put({
|
// TODO: check if it works without the preimage and paymentHash
|
||||||
paymentHash: res.value.paymentHash,
|
// return {
|
||||||
timestamp: Math.floor(Date.now() / 1000),
|
// preimage: res.value.preimage,
|
||||||
});
|
// paymentHash: res.value.paymentHash,
|
||||||
}
|
// };
|
||||||
|
|
||||||
await refreshBalances();
|
|
||||||
return {
|
|
||||||
preimage: res.value.preimage,
|
|
||||||
paymentHash: res.value.paymentHash,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
case "get_balance": {
|
case "get_balance": {
|
||||||
const breezSDK = await getBreezSDK();
|
|
||||||
const info = await breezSDK.getInfo();
|
|
||||||
return {
|
return {
|
||||||
balance: info.walletInfo.balanceSat * 1000 + get(cashuState).balance *
|
balance: currentWalletState.balance * 1000,
|
||||||
1000,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "list_transactions": {
|
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 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 transactions = await currentWalletState.wallet.listPayments(
|
||||||
const liquidPayments = (await breezSDK.listPayments({
|
|
||||||
fromTimestamp: from_timestamp,
|
|
||||||
toTimestamp: to_timestamp,
|
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
})) as any[];
|
);
|
||||||
|
return { transactions };
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
case "get_info": {
|
case "get_info": {
|
||||||
const breezSDK = await getBreezSDK();
|
|
||||||
const info = await breezSDK.getInfo();
|
|
||||||
const privkey = get(nwcPrivkey);
|
const privkey = get(nwcPrivkey);
|
||||||
if (!privkey) throw new Error("missing_privkey");
|
if (!privkey) throw new Error("missing_privkey");
|
||||||
|
|
||||||
|
@ -349,7 +293,6 @@ async function processRpc(
|
||||||
"list_transactions",
|
"list_transactions",
|
||||||
"get_info",
|
"get_info",
|
||||||
"make_invoice",
|
"make_invoice",
|
||||||
"lookup_invoice",
|
|
||||||
],
|
],
|
||||||
notifications: [],
|
notifications: [],
|
||||||
};
|
};
|
||||||
|
@ -361,7 +304,9 @@ async function processRpc(
|
||||||
throw { code: "INTERNAL", message: "missing_amount" };
|
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(
|
const res = await fetch(
|
||||||
`/.well-known/lnurl-pay/callback/${npub}?amount=${amountMsat}`,
|
`/.well-known/lnurl-pay/callback/${npub}?amount=${amountMsat}`,
|
||||||
);
|
);
|
||||||
|
@ -385,35 +330,6 @@ async function processRpc(
|
||||||
invoice,
|
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:
|
default:
|
||||||
throw { code: "NOT_IMPLEMENTED", message: "unknown_method" };
|
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,38 +5,85 @@ import {
|
||||||
decrypt as nip49decrypt,
|
decrypt as nip49decrypt,
|
||||||
encrypt as nip49encrypt,
|
encrypt as nip49encrypt,
|
||||||
} from "nostr-tools/nip49";
|
} 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";
|
interface MetaEntry {
|
||||||
let cachedMnemonic = $state<{ mnemonic: string | undefined }>({
|
key: string;
|
||||||
mnemonic: undefined,
|
value: string;
|
||||||
});
|
}
|
||||||
|
|
||||||
export const passwordRequest = $state({
|
interface CashuTxn {
|
||||||
pending: false,
|
txId: string;
|
||||||
resolve: undefined as ((password: string) => void) | undefined,
|
paymentType: "receive" | "send";
|
||||||
reject: undefined as ((error: Error) => void) | undefined,
|
amountSat: number;
|
||||||
});
|
timestamp: number;
|
||||||
|
status: "complete";
|
||||||
|
}
|
||||||
|
|
||||||
let previouslyInputPassword = $state<{ pass: string | undefined }>({
|
class CashuDB extends Dexie {
|
||||||
pass: undefined,
|
proofs!: Table<Proof, string>;
|
||||||
});
|
meta!: Table<MetaEntry, string>;
|
||||||
|
txns!: Table<CashuTxn, string>;
|
||||||
|
|
||||||
function requestPassword(): Promise<string> {
|
constructor() {
|
||||||
if (!browser) throw new Error("Password input only available in browser");
|
super("cashu");
|
||||||
if (previouslyInputPassword.pass) {
|
this.version(1).stores({
|
||||||
return Promise.resolve(previouslyInputPassword.pass);
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
async getTxns() {
|
||||||
passwordRequest.pending = true;
|
return this.db.txns.toArray();
|
||||||
passwordRequest.resolve = (pw) => {
|
}
|
||||||
previouslyInputPassword.pass = pw;
|
|
||||||
resolve(pw);
|
async getLastRedeemedCashuQuoteTimestamp() {
|
||||||
};
|
const meta = await this.db.meta.get("lastRedeemedCashuQuoteTimestamp");
|
||||||
passwordRequest.reject = reject;
|
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 {
|
export function createMnemonic(): string {
|
||||||
return generateMnemonic(wordlist, 128);
|
return generateMnemonic(wordlist, 128);
|
||||||
}
|
}
|
||||||
|
@ -65,26 +112,62 @@ export async function decryptMnemonic(
|
||||||
return new TextDecoder().decode(decryptedBytes);
|
return new TextDecoder().decode(decryptedBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function storeEncryptedSeed(encrypted: string) {
|
export function storeEncryptedMnemonic(encrypted: string) {
|
||||||
if (browser) localStorage.setItem(SEED_KEY, encrypted);
|
if (browser) localStorage.setItem(MNEMONIC_KEY, encrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasSeed(): boolean {
|
export function hasMnemonic(): boolean {
|
||||||
if (!browser) return false;
|
if (!browser) return false;
|
||||||
return localStorage.getItem(SEED_KEY) !== null;
|
return localStorage.getItem(MNEMONIC_KEY) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMnemonic(): Promise<string> {
|
export const walletState = writable<
|
||||||
if (cachedMnemonic.mnemonic) return cachedMnemonic.mnemonic;
|
| {
|
||||||
if (!browser) throw new Error("Not in browser");
|
open: false;
|
||||||
const enc = localStorage.getItem(SEED_KEY);
|
wallet: undefined;
|
||||||
if (!enc) throw new Error("Seed not initialised");
|
mnemonic: undefined;
|
||||||
const password = await requestPassword();
|
balance: number;
|
||||||
const mnemonic = await decryptMnemonic(enc, password);
|
pendingBalance: number;
|
||||||
cachedMnemonic.mnemonic = mnemonic;
|
cashuBalance: number;
|
||||||
return mnemonic;
|
}
|
||||||
}
|
| {
|
||||||
|
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) {
|
export async function openWallet(mnemonic: string) {
|
||||||
cachedMnemonic.mnemonic = mnemonic;
|
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">
|
<script lang="ts">
|
||||||
import "$lib/style.css";
|
import "$lib/style.css";
|
||||||
import "iconify-icon";
|
import "iconify-icon";
|
||||||
import { hasSeed, passwordRequest } from "$lib/wallet.svelte";
|
import { hasMnemonic, walletState } from "$lib/wallet.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import PasswordDialog from "$lib/components/PasswordDialog.svelte";
|
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 { start as startNwc } from "$lib/nwc.svelte";
|
||||||
import SplashScreen from "$lib/components/SplashScreen.svelte";
|
import SplashScreen from "$lib/components/SplashScreen.svelte";
|
||||||
import ErrorDialog from "$lib/components/ErrorDialog.svelte";
|
import ErrorDialog from "$lib/components/ErrorDialog.svelte";
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
stack?: string;
|
stack?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
let passwordDialogOpen = $state(false);
|
let { children } = $props();
|
||||||
let showSplash = $state(true);
|
let showSplash = $state(true);
|
||||||
let currentError = $state<AppError | null>(null);
|
let currentError = $state<AppError | null>(null);
|
||||||
let showInstallPrompt = $state(false);
|
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() {
|
function goToSettings() {
|
||||||
goto("/settings");
|
goto("/settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { children } = $props();
|
const isOnSetup = $derived(page.route.id?.startsWith("/setup"));
|
||||||
|
const isOnSettings = $derived(page.route.id?.startsWith("/settings"));
|
||||||
$effect(() => {
|
const showSettingsButton = $derived(
|
||||||
if (passwordRequest.pending && !passwordDialogOpen) {
|
!isOnSetup && !isOnSettings && hasMnemonic()
|
||||||
passwordDialogOpen = true;
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const isOnSetup = $derived($page.route.id?.startsWith("/setup"));
|
|
||||||
const isOnSettings = $derived($page.route.id?.startsWith("/settings"));
|
|
||||||
const showSettingsButton = $derived(!isOnSetup && !isOnSettings && hasSeed());
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -96,7 +79,7 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!hasSeed() && !isOnSetup) {
|
if (!hasMnemonic() && !isOnSetup) {
|
||||||
goto("/setup");
|
goto("/setup");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -121,12 +104,14 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !isOnSetup || !passwordRequest.pending}
|
{#if isOnSetup || $walletState.open}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<PasswordDialog bind:open={passwordDialogOpen} unlock={handleUnlockAttempt} />
|
{#if !$walletState.open && !isOnSetup}
|
||||||
|
<PasswordDialog onunlock={() => startNwc()} />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ErrorDialog error={currentError} onclose={() => (currentError = null)} />
|
<ErrorDialog error={currentError} onclose={() => (currentError = null)} />
|
||||||
|
|
|
@ -1,24 +1,13 @@
|
||||||
<script lang="ts">
|
<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 BalanceDisplay from "$lib/components/BalanceDisplay.svelte";
|
||||||
import PaymentHistory from "$lib/components/PaymentHistory.svelte";
|
import PaymentHistory from "$lib/components/PaymentHistory.svelte";
|
||||||
import ReceiveDialog from "$lib/components/ReceiveDialog.svelte";
|
import ReceiveDialog from "$lib/components/ReceiveDialog.svelte";
|
||||||
import SendDialog from "$lib/components/SendDialog.svelte";
|
import SendDialog from "$lib/components/SendDialog.svelte";
|
||||||
import Button from "$lib/components/Button.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 receiveDialogOpen = $state(false);
|
||||||
let sendDialogOpen = $state(false);
|
let sendDialogOpen = $state(false);
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
initBalances();
|
|
||||||
breezSDK = await getBreezSDK();
|
|
||||||
});
|
|
||||||
|
|
||||||
function openReceiveDialog() {
|
function openReceiveDialog() {
|
||||||
receiveDialogOpen = true;
|
receiveDialogOpen = true;
|
||||||
}
|
}
|
||||||
|
@ -26,20 +15,13 @@
|
||||||
function openSendDialog() {
|
function openSendDialog() {
|
||||||
sendDialogOpen = true;
|
sendDialogOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPaymentSent() {
|
|
||||||
refreshBalances();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ReceiveDialog bind:open={receiveDialogOpen} />
|
<ReceiveDialog bind:open={receiveDialogOpen} />
|
||||||
<SendDialog bind:open={sendDialogOpen} onsent={onPaymentSent} />
|
<SendDialog bind:open={sendDialogOpen} />
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<BalanceDisplay
|
<BalanceDisplay />
|
||||||
balance={balances.balance}
|
|
||||||
pending={balances.pendingReceive - balances.pendingSend}
|
|
||||||
/>
|
|
||||||
<div class="retro-card send-receive-buttons">
|
<div class="retro-card send-receive-buttons">
|
||||||
<Button variant="primary" onclick={openReceiveDialog}>Receive</Button>
|
<Button variant="primary" onclick={openReceiveDialog}>Receive</Button>
|
||||||
<Button variant="danger" onclick={openSendDialog}>Send</Button>
|
<Button variant="danger" onclick={openSendDialog}>Send</Button>
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
import {
|
import {
|
||||||
createMnemonic,
|
createMnemonic,
|
||||||
encryptMnemonic,
|
encryptMnemonic,
|
||||||
storeEncryptedSeed,
|
storeEncryptedMnemonic,
|
||||||
cacheMnemonic,
|
|
||||||
isValidMnemonic,
|
isValidMnemonic,
|
||||||
|
openWallet,
|
||||||
} from "$lib/wallet.svelte";
|
} from "$lib/wallet.svelte";
|
||||||
import { start as startNwc } from "$lib/nwc.svelte";
|
import { start as startNwc } from "$lib/nwc.svelte";
|
||||||
|
|
||||||
|
@ -77,8 +77,8 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const encrypted = await encryptMnemonic(userMnemonic, password);
|
const encrypted = await encryptMnemonic(userMnemonic, password);
|
||||||
storeEncryptedSeed(encrypted);
|
storeEncryptedMnemonic(encrypted);
|
||||||
cacheMnemonic(userMnemonic);
|
await openWallet(userMnemonic);
|
||||||
|
|
||||||
startNwc();
|
startNwc();
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue