From 13466248004327e9a026cb89f94926f9b060d1c1 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 11 Feb 2026 17:51:24 -0800 Subject: linting --- src/lib/ark.ts | 240 +++++++------- src/lib/bip21.ts | 82 ++--- src/lib/cache.ts | 3 +- src/lib/handler.ts | 16 +- src/lib/invoice.ts | 54 ++-- src/lib/locales.ts | 2 +- src/lib/nip01.ts | 26 +- src/lib/nip07.ts | 79 +++-- src/lib/nip11.ts | 18 +- src/lib/nip17.ts | 638 ++++++++++++++++++++---------------- src/lib/nip40.ts | 18 +- src/lib/nip44.ts | 298 +++++++++-------- src/lib/nip51.ts | 113 ++++--- src/lib/nostr.ts | 360 ++++++++++----------- src/lib/parse.ts | 260 +++++++-------- src/lib/passwordCache.ts | 79 ++--- src/lib/punks.ts | 128 ++++---- src/lib/random.ts | 28 +- src/lib/rates.ts | 10 +- src/lib/samples.ts | 34 +- src/lib/settings.ts | 34 +- src/lib/socket.ts | 152 ++++----- src/lib/store.ts | 110 +++---- src/lib/ticket.ts | 44 +-- src/lib/tickets.ts | 100 +++--- src/lib/types.ts | 24 +- src/lib/upload.ts | 30 +- src/lib/utils.ts | 821 ++++++++++++++++++++++++----------------------- 28 files changed, 1981 insertions(+), 1820 deletions(-) (limited to 'src/lib') diff --git a/src/lib/ark.ts b/src/lib/ark.ts index d045049e..51e378c3 100644 --- a/src/lib/ark.ts +++ b/src/lib/ark.ts @@ -4,8 +4,8 @@ import { post } from "$lib/utils"; import { get } from "svelte/store"; export const arkServerUrl = { - bitcoin: "https://arkade.computer", - regtest: "http://localhost:7070", + bitcoin: "https://arkade.computer", + regtest: "http://localhost:7070", }[PUBLIC_COINOS_NETWORK || "regtest"]; export const arkkey = persistLocal("arkkey", ""); @@ -16,166 +16,166 @@ let walletInstance: any; let walletKey: string | undefined; export const getWallet = async () => { - const key = get(arkkey); - if (!key) { - walletInstance = undefined; - walletKey = undefined; - return; - } - if (walletInstance && walletKey === key) return walletInstance; - - const { SingleKey, Wallet } = await loadArkSdk(); - const identity = SingleKey.fromHex(key); - - walletInstance = await Wallet.create({ - identity, - arkServerUrl, - }); - walletKey = key; - return walletInstance; + const key = get(arkkey); + if (!key) { + walletInstance = undefined; + walletKey = undefined; + return; + } + if (walletInstance && walletKey === key) return walletInstance; + + const { SingleKey, Wallet } = await loadArkSdk(); + const identity = SingleKey.fromHex(key); + + walletInstance = await Wallet.create({ + identity, + arkServerUrl, + }); + walletKey = key; + return walletInstance; }; export const getAddress = async (): Promise => { - const wallet = await getWallet(); - return wallet.getAddress(); + const wallet = await getWallet(); + return wallet.getAddress(); }; export const getBalance = async (): Promise<{ available: number }> => { - const wallet = await getWallet(); - return wallet.getBalance(); + const wallet = await getWallet(); + return wallet.getBalance(); }; export const getVtxos = async () => { - const wallet = await getWallet(); - if (!wallet) return []; - return wallet.getVtxos({ spendableOnly: false }); + const wallet = await getWallet(); + if (!wallet) return []; + return wallet.getVtxos({ spendableOnly: false }); }; export const syncTransactions = async (aid: string) => { - const wallet = await getWallet(); - if (!wallet) return; - - const transactions: any[] = []; - - const history = await wallet.getTransactionHistory(); - const historyTxids = new Set(); - console.log("arkSync: history", history.length, history); - - for (const tx of history) { - if (tx.key.arkTxid) historyTxids.add(tx.key.arkTxid); - if (tx.key.commitmentTxid) historyTxids.add(tx.key.commitmentTxid); - transactions.push({ - arkTxid: tx.key.arkTxid || null, - commitmentTxid: tx.key.commitmentTxid || null, - amount: tx.type === "RECEIVED" ? tx.amount : -tx.amount, - settled: tx.settled, - createdAt: tx.createdAt, - }); - } - - // Include preconfirmed VTXOs not yet in transaction history - const vtxos = await wallet.getVtxos({ spendableOnly: false }); - console.log("arkSync: vtxos", vtxos.length, vtxos); - for (const v of vtxos) { - if (v.virtualStatus?.state !== "preconfirmed") continue; - const id = v.arkTxId || v.txid; - if (!id || historyTxids.has(id)) continue; - historyTxids.add(id); - transactions.push({ - arkTxid: v.arkTxId || null, - commitmentTxid: v.txid || null, - amount: Number(v.value), - settled: false, - createdAt: v.createdAt ? new Date(v.createdAt).getTime() : Date.now(), - }); - } - - if (!transactions.length) return; - - return post("/post/ark/sync", { transactions, aid }); + const wallet = await getWallet(); + if (!wallet) return; + + const transactions: any[] = []; + + const history = await wallet.getTransactionHistory(); + const historyTxids = new Set(); + console.log("arkSync: history", history.length, history); + + for (const tx of history) { + if (tx.key.arkTxid) historyTxids.add(tx.key.arkTxid); + if (tx.key.commitmentTxid) historyTxids.add(tx.key.commitmentTxid); + transactions.push({ + arkTxid: tx.key.arkTxid || null, + commitmentTxid: tx.key.commitmentTxid || null, + amount: tx.type === "RECEIVED" ? tx.amount : -tx.amount, + settled: tx.settled, + createdAt: tx.createdAt, + }); + } + + // Include preconfirmed VTXOs not yet in transaction history + const vtxos = await wallet.getVtxos({ spendableOnly: false }); + console.log("arkSync: vtxos", vtxos.length, vtxos); + for (const v of vtxos) { + if (v.virtualStatus?.state !== "preconfirmed") continue; + const id = v.arkTxId || v.txid; + if (!id || historyTxids.has(id)) continue; + historyTxids.add(id); + transactions.push({ + arkTxid: v.arkTxId || null, + commitmentTxid: v.txid || null, + amount: Number(v.value), + settled: false, + createdAt: v.createdAt ? new Date(v.createdAt).getTime() : Date.now(), + }); + } + + if (!transactions.length) return; + + return post("/post/ark/sync", { transactions, aid }); }; export const vtxoKey = (v: { txid: string; vout: number }) => - `${v.txid}:${v.vout}`; + `${v.txid}:${v.vout}`; export const getProcessedVtxos = (): Set => { - try { - return new Set( - JSON.parse(localStorage.getItem("processedArkVtxos") || "[]"), - ); - } catch { - return new Set(); - } + try { + return new Set( + JSON.parse(localStorage.getItem("processedArkVtxos") || "[]"), + ); + } catch { + return new Set(); + } }; export const addProcessedVtxos = (keys: string[]) => { - const processed = getProcessedVtxos(); - for (const k of keys) processed.add(k); - const arr = [...processed].slice(-200); - localStorage.setItem("processedArkVtxos", JSON.stringify(arr)); + const processed = getProcessedVtxos(); + for (const k of keys) processed.add(k); + const arr = [...processed].slice(-200); + localStorage.setItem("processedArkVtxos", JSON.stringify(arr)); }; // Flag to suppress notifyIncomingFunds during sends export let arkSending = false; export const sendArk = async (address: string, amount: number) => { - const wallet = await getWallet(); - if (!wallet) throw new Error("Ark wallet not available"); + const wallet = await getWallet(); + if (!wallet) throw new Error("Ark wallet not available"); - // Suppress incoming notifications during send to prevent - // change VTXOs from being credited as incoming payments - arkSending = true; + // Suppress incoming notifications during send to prevent + // change VTXOs from being credited as incoming payments + arkSending = true; - try { - const txid = await wallet.sendBitcoin({ address, amount }); + try { + const txid = await wallet.sendBitcoin({ address, amount }); - // Mark post-send VTXOs as processed so notifyIncomingFunds - // ignores change VTXOs from this send - const vtxos = await wallet.getVtxos({ spendableOnly: false }); - if (vtxos?.length) addProcessedVtxos(vtxos.map(vtxoKey)); + // Mark post-send VTXOs as processed so notifyIncomingFunds + // ignores change VTXOs from this send + const vtxos = await wallet.getVtxos({ spendableOnly: false }); + if (vtxos?.length) addProcessedVtxos(vtxos.map(vtxoKey)); - return txid; - } finally { - arkSending = false; - } + return txid; + } finally { + arkSending = false; + } }; export const sendArkViaForward = async ({ - serverArkAddress, - amount, - aid, - forward, - user, + serverArkAddress, + amount, + aid, + forward, + user, }: { - serverArkAddress: string; - amount: number; - aid: string; - forward: string; - user: any; + serverArkAddress: string; + amount: number; + aid: string; + forward: string; + user: any; }) => { - const txid = await sendArk(serverArkAddress, amount); + const txid = await sendArk(serverArkAddress, amount); - await post("/post/ark/vault-send", { hash: txid, amount, aid }); + await post("/post/ark/vault-send", { hash: txid, amount, aid }); - const inv = await post("/post/invoice", { - invoice: { type: "ark", amount, forward, aid }, - user, - }); + const inv = await post("/post/invoice", { + invoice: { type: "ark", amount, forward, aid }, + user, + }); - return post("/post/ark/receive", { amount, hash: txid, iid: inv.id }); + return post("/post/ark/receive", { amount, hash: txid, iid: inv.id }); }; export const settle = async () => { - const wallet = await getWallet(); - if (!wallet) return; + const wallet = await getWallet(); + if (!wallet) return; - const balance = await wallet.getBalance(); - if (!balance.preconfirmed) return; + const balance = await wallet.getBalance(); + if (!balance.preconfirmed) return; - await wallet.settle(); + await wallet.settle(); - // Mark post-settle VTXOs as processed so notifyIncomingFunds - // ignores renewed VTXOs (they get new txids after an Ark round) - const vtxos = await wallet.getVtxos({ spendableOnly: false }); - if (vtxos?.length) addProcessedVtxos(vtxos.map(vtxoKey)); + // Mark post-settle VTXOs as processed so notifyIncomingFunds + // ignores renewed VTXOs (they get new txids after an Ark round) + const vtxos = await wallet.getVtxos({ spendableOnly: false }); + if (vtxos?.length) addProcessedVtxos(vtxos.map(vtxoKey)); }; diff --git a/src/lib/bip21.ts b/src/lib/bip21.ts index 4689246e..842782a5 100644 --- a/src/lib/bip21.ts +++ b/src/lib/bip21.ts @@ -1,50 +1,50 @@ export function decode(uri, allowedSchemes = ["bitcoin", "liquidnetwork"]) { - // Determine the scheme from the URI - const split = uri.indexOf(":"); - if (split === -1) { - throw new Error("Invalid URI: Missing scheme"); - } - - const urnScheme = uri.slice(0, split).toLowerCase(); - if (!allowedSchemes.includes(urnScheme)) { - throw new Error(`Invalid scheme: ${urnScheme}`); - } - - // Extract address and query string - const queryStart = uri.indexOf("?"); - const address = uri.slice( - split + 1, - queryStart === -1 ? undefined : queryStart, - ); - const query = queryStart === -1 ? "" : uri.slice(queryStart + 1); - const options = Object.fromEntries(new URLSearchParams(query)); - - if (options.amount) { - const n = Number(options.amount); - if (!Number.isFinite(n)) throw new Error("Invalid amount"); - if (n < 0) throw new Error("Invalid amount"); - } - - return { scheme: urnScheme, address, options }; + // Determine the scheme from the URI + const split = uri.indexOf(":"); + if (split === -1) { + throw new Error("Invalid URI: Missing scheme"); + } + + const urnScheme = uri.slice(0, split).toLowerCase(); + if (!allowedSchemes.includes(urnScheme)) { + throw new Error(`Invalid scheme: ${urnScheme}`); + } + + // Extract address and query string + const queryStart = uri.indexOf("?"); + const address = uri.slice( + split + 1, + queryStart === -1 ? undefined : queryStart, + ); + const query = queryStart === -1 ? "" : uri.slice(queryStart + 1); + const options = Object.fromEntries(new URLSearchParams(query)); + + if (options.amount) { + const n = Number(options.amount); + if (!Number.isFinite(n)) throw new Error("Invalid amount"); + if (n < 0) throw new Error("Invalid amount"); + } + + return { scheme: urnScheme, address, options }; } export function encode( - address, - options = { amount: "" }, - urnScheme = "bitcoin", + address, + options = { amount: "" }, + urnScheme = "bitcoin", ) { - if (!address || typeof address !== "string") { - throw new TypeError("Invalid address"); - } + if (!address || typeof address !== "string") { + throw new TypeError("Invalid address"); + } - if (!["bitcoin", "liquidnetwork"].includes(urnScheme)) { - throw new Error(`Unsupported scheme: ${urnScheme}`); - } + if (!["bitcoin", "liquidnetwork"].includes(urnScheme)) { + throw new Error(`Unsupported scheme: ${urnScheme}`); + } - const n = Number(options?.amount); - if (!Number.isFinite(n)) throw new Error("Invalid amount"); - if (n < 0) throw new Error("Invalid amount"); + const n = Number(options?.amount); + if (!Number.isFinite(n)) throw new Error("Invalid amount"); + if (n < 0) throw new Error("Invalid amount"); - const query = new URLSearchParams(options).toString(); - return `${urnScheme}:${address}${query ? `?${query}` : ""}`; + const query = new URLSearchParams(options).toString(); + return `${urnScheme}:${address}${query ? `?${query}` : ""}`; } diff --git a/src/lib/cache.ts b/src/lib/cache.ts index 079c561b..064b4632 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -1,10 +1,9 @@ - // src/lib/server/cache.ts let cachedData: any = null; async function fetchData() { // Fetch data from your API - const response = await fetch('https://api.example.com/data'); + const response = await fetch("https://api.example.com/data"); cachedData = await response.json(); } diff --git a/src/lib/handler.ts b/src/lib/handler.ts index baca442d..559761f0 100644 --- a/src/lib/handler.ts +++ b/src/lib/handler.ts @@ -2,12 +2,12 @@ import { goto } from "$app/navigation"; import { applyAction } from "$app/forms"; export default (toggle) => () => { - toggle(); - return async ({ result }) => { - if (result.type === "redirect") { - goto(result.location); - } else { - await applyAction(result); - } - }; + toggle(); + return async ({ result }) => { + if (result.type === "redirect") { + goto(result.location); + } else { + await applyAction(result); + } + }; }; diff --git a/src/lib/invoice.ts b/src/lib/invoice.ts index 91a69d88..015f2837 100644 --- a/src/lib/invoice.ts +++ b/src/lib/invoice.ts @@ -3,37 +3,37 @@ import { auth, post } from "$lib/utils"; import { redirect } from "@sveltejs/kit"; export default async ({ cookies, request }) => { - const form = await request.formData(); - const aid = form.get("aid") || cookies.get("aid"); + const form = await request.formData(); + const aid = form.get("aid") || cookies.get("aid"); - const amount = parseInt(form.get("amount")); - const rates = await getRates(); + const amount = parseInt(form.get("amount")); + const rates = await getRates(); - let invoice = { - aid, - amount, - items: JSON.parse(form.get("items")), - memo: form.get("memo"), - memoPrompt: form.get("memoPrompt"), - tip: form.get("tip") ?? null, - type: form.get("type"), - prompt: form.get("prompt") === "true", - rate: parseFloat(form.get("rate")) || rates[form.get("currency")], - id: undefined, - }; + let invoice = { + aid, + amount, + items: JSON.parse(form.get("items")), + memo: form.get("memo"), + memoPrompt: form.get("memoPrompt"), + tip: form.get("tip") ?? null, + type: form.get("type"), + prompt: form.get("prompt") === "true", + rate: parseFloat(form.get("rate")) || rates[form.get("currency")], + id: undefined, + }; - const user = { - username: form.get("username"), - currency: form.get("currency"), - }; + const user = { + username: form.get("username"), + currency: form.get("currency"), + }; - invoice = await post("/invoice", { invoice, user }, auth(cookies)); - const { id } = invoice; + invoice = await post("/invoice", { invoice, user }, auth(cookies)); + const { id } = invoice; - if (invoice.prompt && invoice.tip === null) - redirect(307, `/invoice/${id}/tip`); + if (invoice.prompt && invoice.tip === null) + redirect(307, `/invoice/${id}/tip`); - if (invoice.memoPrompt && !invoice.memo) { - redirect(307, `/invoice/${id}/memo`); - } else redirect(307, `/invoice/${id}`); + if (invoice.memoPrompt && !invoice.memo) { + redirect(307, `/invoice/${id}/memo`); + } else redirect(307, `/invoice/${id}`); }; diff --git a/src/lib/locales.ts b/src/lib/locales.ts index 282eac22..83e7a1bf 100644 --- a/src/lib/locales.ts +++ b/src/lib/locales.ts @@ -37,5 +37,5 @@ export default { it, nl, th, - pl + pl, }; diff --git a/src/lib/nip01.ts b/src/lib/nip01.ts index a5d3f4b5..b2718b6b 100644 --- a/src/lib/nip01.ts +++ b/src/lib/nip01.ts @@ -1,19 +1,21 @@ -import { PUBLIC_DM_RELAYS, PUBLIC_COINOS_RELAY } from '$env/static/public'; -const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); -const USERINFO_RELAYS = [ ...DM_RELAYS_LIST, PUBLIC_COINOS_RELAY ]; +import { PUBLIC_DM_RELAYS, PUBLIC_COINOS_RELAY } from "$env/static/public"; +const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(","); +const USERINFO_RELAYS = [...DM_RELAYS_LIST, PUBLIC_COINOS_RELAY]; -import { SimplePool } from 'nostr-tools/pool'; +import { SimplePool } from "nostr-tools/pool"; const pool = new SimplePool(); // Returns the content of the kind-0 event of the provided pubkey. export const getNostrUserInfo = async (pubkey: string) => { - const events = await pool.querySync( - USERINFO_RELAYS, { kinds: [0], limit: 1, authors: [pubkey] } - ); + const events = await pool.querySync(USERINFO_RELAYS, { + kinds: [0], + limit: 1, + authors: [pubkey], + }); - if (events.length == 0) { - return null; - } + if (events.length == 0) { + return null; + } - return JSON.parse(events[0].content); -} + return JSON.parse(events[0].content); +}; diff --git a/src/lib/nip07.ts b/src/lib/nip07.ts index 85f68377..4b32512f 100644 --- a/src/lib/nip07.ts +++ b/src/lib/nip07.ts @@ -1,41 +1,52 @@ -import { bytesToHex } from '@noble/hashes/utils.js'; -import { finalizeEvent } from 'nostr-tools/pure'; -import { getPrivateKey } from '$lib/nostr'; -import * as libnip44 from '$lib/nip44'; -import { u } from '$lib/nip44'; +import { bytesToHex } from "@noble/hashes/utils.js"; +import { finalizeEvent } from "nostr-tools/pure"; +import { getPrivateKey } from "$lib/nostr"; +import * as libnip44 from "$lib/nip44"; +import { u } from "$lib/nip44"; // Signs an event using NIP-07 if available, otherwise getPrivateKey() export const signEvent = async (event: any, user: any): Promise => { - if (await (window as any).nostr.getPublicKey() === user.pubkey) { - return (window as any).nostr.signEvent(event); - } else { - const sk = await getPrivateKey(user); - return finalizeEvent(event, sk); - } -} + if ((await (window as any).nostr.getPublicKey()) === user.pubkey) { + return (window as any).nostr.signEvent(event); + } else { + const sk = await getPrivateKey(user); + return finalizeEvent(event, sk); + } +}; // Encrypts an event in NIP-44 using NIP-07 if available, otherwise getPrivateKey() -export const encrypt = async (json: any, user: any, receiverPK: string): Promise => { - if (await (window as any).nostr.getPublicKey() === user.pubkey) { - return (window as any).nostr.nip44.encrypt( - receiverPK, JSON.stringify(json)); - } else { - const senderSK = await getPrivateKey(user); - const senderSKString = bytesToHex(senderSK); - const ckey = u.getConversationKey(senderSKString, receiverPK); - return libnip44.encrypt(JSON.stringify(json), ckey); - } -} +export const encrypt = async ( + json: any, + user: any, + receiverPK: string, +): Promise => { + if ((await (window as any).nostr.getPublicKey()) === user.pubkey) { + return (window as any).nostr.nip44.encrypt( + receiverPK, + JSON.stringify(json), + ); + } else { + const senderSK = await getPrivateKey(user); + const senderSKString = bytesToHex(senderSK); + const ckey = u.getConversationKey(senderSKString, receiverPK); + return libnip44.encrypt(JSON.stringify(json), ckey); + } +}; // Decrypts an event in NIP-44 using NIP-07 if available, otherwise getPrivateKey() -export const decrypt = async (encrypted: string, user: any, receiverPK: string): Promise => { - if (await (window as any).nostr.getPublicKey() === user.pubkey) { - return JSON.parse(await (window as any).nostr.nip44.decrypt( - receiverPK, encrypted)); - } else { - const senderSK = await getPrivateKey(user); - const senderSKString = bytesToHex(senderSK); - const ckey = u.getConversationKey(senderSKString, receiverPK); - return JSON.parse(libnip44.decrypt(encrypted, ckey)); - } -} +export const decrypt = async ( + encrypted: string, + user: any, + receiverPK: string, +): Promise => { + if ((await (window as any).nostr.getPublicKey()) === user.pubkey) { + return JSON.parse( + await (window as any).nostr.nip44.decrypt(receiverPK, encrypted), + ); + } else { + const senderSK = await getPrivateKey(user); + const senderSKString = bytesToHex(senderSK); + const ckey = u.getConversationKey(senderSKString, receiverPK); + return JSON.parse(libnip44.decrypt(encrypted, ckey)); + } +}; diff --git a/src/lib/nip11.ts b/src/lib/nip11.ts index af254b7e..6e827ccb 100644 --- a/src/lib/nip11.ts +++ b/src/lib/nip11.ts @@ -1,15 +1,15 @@ -import { fetchRelayInformation } from 'nostr-tools/nip11'; +import { fetchRelayInformation } from "nostr-tools/nip11"; // Accepts a list of relays, and returns a list of the relays that support all the provided NIPs. export const relaysSupporting = async (relays: string[], nips: number[]) => { - let supportingRelays = []; + let supportingRelays = []; - for (const relay of relays) { - const relayInfo = await fetchRelayInformation(relay); - if (nips.every(relay => relayInfo.supported_nips.includes(relay))) { - supportingRelays.push(relay); - } + for (const relay of relays) { + const relayInfo = await fetchRelayInformation(relay); + if (nips.every((relay) => relayInfo.supported_nips.includes(relay))) { + supportingRelays.push(relay); } + } - return supportingRelays; -} + return supportingRelays; +}; diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index b039ebf8..32a18bd8 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -1,84 +1,101 @@ -import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js'; - -import { finalizeEvent, getEventHash, generateSecretKey, getPublicKey } from 'nostr-tools/pure'; -import { SimplePool } from 'nostr-tools/pool'; -import { Relay } from 'nostr-tools/relay'; -import { unwrapEvent } from 'nostr-tools/nip17'; - -import { relaysSupporting } from '$lib/nip11'; -import { expired, expiration } from '$lib/nip40'; -import { encrypt, u } from '$lib/nip44'; -import { eventToSign, signer } from '$lib/store'; -import { get } from 'svelte/store'; +import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js"; + +import { + finalizeEvent, + getEventHash, + generateSecretKey, + getPublicKey, +} from "nostr-tools/pure"; +import { SimplePool } from "nostr-tools/pool"; +import { Relay } from "nostr-tools/relay"; +import { unwrapEvent } from "nostr-tools/nip17"; + +import { relaysSupporting } from "$lib/nip11"; +import { expired, expiration } from "$lib/nip40"; +import { encrypt, u } from "$lib/nip44"; +import { eventToSign, signer } from "$lib/store"; +import { get } from "svelte/store"; const TWO_DAYS = 2 * 24 * 60 * 60; -const randomTimestamp = () => Math.floor( - Date.now() / 1000 - Math.random() * TWO_DAYS); +const randomTimestamp = () => + Math.floor(Date.now() / 1000 - Math.random() * TWO_DAYS); const pool = new SimplePool(); let relayListCache = new Map(); -import { PUBLIC_DM_RELAYS } from '$env/static/public'; -const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); +import { PUBLIC_DM_RELAYS } from "$env/static/public"; +const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(","); const DM_FETCH_LIMIT = 256; // Ensures we have access to a secret key, triggering the signer modal if needed. // Returns the sk as Uint8Array, or null if using NIP-07 extension. const ensureSigner = async (userPubkey: string): Promise => { - // Check if NIP-07 extension is available and matches user - if ((window as any).nostr) { - try { - const extPubkey = await (window as any).nostr.getPublicKey(); - if (extPubkey === userPubkey) { - return null; // Use NIP-07 - } - } catch (e) { - // Extension didn't work, fall through to signer - } - } - - // Check if signer already has a valid sk (64 hex chars = 32 bytes) - const currentSigner: any = get(signer); - if (currentSigner?.ready && currentSigner?.params?.sk && currentSigner.params.sk.length === 64) { - return hexToBytes(currentSigner.params.sk); - } - - // Clear invalid signer state before triggering modal - if (currentSigner?.ready && (!currentSigner?.params?.sk || currentSigner.params.sk.length !== 64)) { - signer.set(undefined); + // Check if NIP-07 extension is available and matches user + if ((window as any).nostr) { + try { + const extPubkey = await (window as any).nostr.getPublicKey(); + if (extPubkey === userPubkey) { + return null; // Use NIP-07 + } + } catch { + // Extension didn't work, fall through to signer } - - // Trigger the signer modal with a dummy event - const dummyEvent = { kind: 14, content: 'DM encryption', tags: [], created_at: Math.floor(Date.now() / 1000) }; - eventToSign.set(dummyEvent); - - // Wait for signer to be ready - const signerValue = await new Promise((resolve, reject) => { - let unsubscribe: () => void; - unsubscribe = signer.subscribe((v: any) => { - if (v === 'cancel') { - setTimeout(() => unsubscribe?.()); - eventToSign.set(null); - reject(new Error('cancelled')); - } - if (v?.ready) { - setTimeout(() => unsubscribe?.()); - eventToSign.set(null); - resolve(v); - } - }); + } + + // Check if signer already has a valid sk (64 hex chars = 32 bytes) + const currentSigner: any = get(signer); + if ( + currentSigner?.ready && + currentSigner?.params?.sk && + currentSigner.params.sk.length === 64 + ) { + return hexToBytes(currentSigner.params.sk); + } + + // Clear invalid signer state before triggering modal + if ( + currentSigner?.ready && + (!currentSigner?.params?.sk || currentSigner.params.sk.length !== 64) + ) { + signer.set(undefined); + } + + // Trigger the signer modal with a dummy event + const dummyEvent = { + kind: 14, + content: "DM encryption", + tags: [], + created_at: Math.floor(Date.now() / 1000), + }; + eventToSign.set(dummyEvent); + + // Wait for signer to be ready + const signerValue = await new Promise((resolve, reject) => { + let unsubscribe: () => void; + unsubscribe = signer.subscribe((v: any) => { + if (v === "cancel") { + setTimeout(() => unsubscribe?.()); + eventToSign.set(null); + reject(new Error("cancelled")); + } + if (v?.ready) { + setTimeout(() => unsubscribe?.()); + eventToSign.set(null); + resolve(v); + } }); + }); - if (signerValue?.params?.sk) { - return hexToBytes(signerValue.params.sk); - } + if (signerValue?.params?.sk) { + return hexToBytes(signerValue.params.sk); + } - // If extension method was selected - if (signerValue?.method === 'extension') { - return null; // Use NIP-07 - } + // If extension method was selected + if (signerValue?.method === "extension") { + return null; // Use NIP-07 + } - throw new Error('No signing method available'); -} + throw new Error("No signing method available"); +}; // Create a NIP-17 message using a provided sender secret key. // If wrapPK is specified and different than receiverPK, @@ -87,11 +104,22 @@ const ensureSigner = async (userPubkey: string): Promise => { // If expiryDays is specified, the message will expire in that many days, // plus a random amount from 0 to 2 for security. // (expiration time = random creation time + expiryDays + 2 days) -export const createNIP17MessageSK = (text: string, senderSK: Uint8Array, receiverPK: string, wrapPK: string | null = null, expiryDays: number | null = null) => { - const rumour = createRumour(text, getPublicKey(senderSK), receiverPK); - const sealed = sealRumourSK(rumour, senderSK, wrapPK || receiverPK, expiryDays); - return giftWrap(sealed, wrapPK || receiverPK, expiryDays); -} +export const createNIP17MessageSK = ( + text: string, + senderSK: Uint8Array, + receiverPK: string, + wrapPK: string | null = null, + expiryDays: number | null = null, +) => { + const rumour = createRumour(text, getPublicKey(senderSK), receiverPK); + const sealed = sealRumourSK( + rumour, + senderSK, + wrapPK || receiverPK, + expiryDays, + ); + return giftWrap(sealed, wrapPK || receiverPK, expiryDays); +}; // Create a NIP-17 message using NIP-07, requiring no sender secret key. // If wrapPK is specified and different than receiverPK, @@ -100,241 +128,311 @@ export const createNIP17MessageSK = (text: string, senderSK: Uint8Array, receive // If expiryDays is specified, the message will expire in that many days, // plus a random amount from 0 to 2 for security. // (expiration time = random creation time + expiryDays + 2 days) -export const createNIP17MessageNIP07 = async (text: string, senderPK: string, receiverPK: string, wrapPK: string | null = null, expiryDays: number | null = null) => { - const rumour = createRumour(text, senderPK, receiverPK); - const sealed = await sealRumourNip07(rumour, wrapPK || receiverPK, expiryDays); - return giftWrap(sealed, wrapPK || receiverPK, expiryDays); -} +export const createNIP17MessageNIP07 = async ( + text: string, + senderPK: string, + receiverPK: string, + wrapPK: string | null = null, + expiryDays: number | null = null, +) => { + const rumour = createRumour(text, senderPK, receiverPK); + const sealed = await sealRumourNip07( + rumour, + wrapPK || receiverPK, + expiryDays, + ); + return giftWrap(sealed, wrapPK || receiverPK, expiryDays); +}; const createRumour = (text: string, senderPK: string, receiverPK: string) => { - const rumour: any = { - kind: 14, // rumour - created_at: Math.floor(Date.now() / 1000), - tags: [["p", receiverPK]], - pubkey: senderPK, - content: text - }; - rumour.id = getEventHash(rumour); // do NOT sign rumours! - return rumour; -} - -const sealRumourSK = (rumour: any, senderSK: Uint8Array, receiverPK: string, expiryDays: number | null = null) => { - const senderSKString = bytesToHex(senderSK); - const conversationKey = u.getConversationKey(senderSKString, receiverPK); - const encryptedRumour = encrypt(JSON.stringify(rumour), conversationKey); - - const created = randomTimestamp(); - const sealEvent: any = { - kind: 13, // seal - created_at: created, - tags: [], - pubkey: getPublicKey(senderSK), - content: encryptedRumour - } - if (expiryDays != null) { - const expiration = created + (expiryDays + 2) * 86400; - sealEvent.tags.push(["expiration", expiration.toString()]); - } - - return finalizeEvent(sealEvent, senderSK); -} - -const sealRumourNip07 = async (rumour: any, receiverPK: string, expiryDays: number | null = null) => { - const encryptedRumour = await (window as any).nostr.nip44.encrypt( - receiverPK, JSON.stringify(rumour)); + const rumour: any = { + kind: 14, // rumour + created_at: Math.floor(Date.now() / 1000), + tags: [["p", receiverPK]], + pubkey: senderPK, + content: text, + }; + rumour.id = getEventHash(rumour); // do NOT sign rumours! + return rumour; +}; - const created = randomTimestamp(); - const sealEvent: any = { - kind: 13, // seal - created_at: created, - tags: [], - content: encryptedRumour - } - if (expiryDays != null) { - const expiration = created + (expiryDays + 2) * 86400; - sealEvent.tags.push(["expiration", expiration.toString()]); - } +const sealRumourSK = ( + rumour: any, + senderSK: Uint8Array, + receiverPK: string, + expiryDays: number | null = null, +) => { + const senderSKString = bytesToHex(senderSK); + const conversationKey = u.getConversationKey(senderSKString, receiverPK); + const encryptedRumour = encrypt(JSON.stringify(rumour), conversationKey); + + const created = randomTimestamp(); + const sealEvent: any = { + kind: 13, // seal + created_at: created, + tags: [], + pubkey: getPublicKey(senderSK), + content: encryptedRumour, + }; + if (expiryDays != null) { + const expiration = created + (expiryDays + 2) * 86400; + sealEvent.tags.push(["expiration", expiration.toString()]); + } + + return finalizeEvent(sealEvent, senderSK); +}; - return (window as any).nostr.signEvent(sealEvent); -} - -const giftWrap = (event: any, receiverPK: string, expiryDays: number | null = null) => { - const secretKey = generateSecretKey(); - const conversationKey = u.getConversationKey(bytesToHex(secretKey), receiverPK); - const encryptedEvent = encrypt(JSON.stringify(event), conversationKey); - const created = randomTimestamp(); - - const giftWrapEvent: any = { - kind: 1059, // gift wrap - created_at: created, - tags: [["p", receiverPK]], - pubkey: getPublicKey(secretKey), - content: encryptedEvent - } - if (expiryDays != null) { - const expiration = created + (expiryDays + 2) * 86400; - giftWrapEvent.tags.push(["expiration", expiration.toString()]); - } +const sealRumourNip07 = async ( + rumour: any, + receiverPK: string, + expiryDays: number | null = null, +) => { + const encryptedRumour = await (window as any).nostr.nip44.encrypt( + receiverPK, + JSON.stringify(rumour), + ); + + const created = randomTimestamp(); + const sealEvent: any = { + kind: 13, // seal + created_at: created, + tags: [], + content: encryptedRumour, + }; + if (expiryDays != null) { + const expiration = created + (expiryDays + 2) * 86400; + sealEvent.tags.push(["expiration", expiration.toString()]); + } + + return (window as any).nostr.signEvent(sealEvent); +}; - return finalizeEvent(giftWrapEvent, secretKey); -} +const giftWrap = ( + event: any, + receiverPK: string, + expiryDays: number | null = null, +) => { + const secretKey = generateSecretKey(); + const conversationKey = u.getConversationKey( + bytesToHex(secretKey), + receiverPK, + ); + const encryptedEvent = encrypt(JSON.stringify(event), conversationKey); + const created = randomTimestamp(); + + const giftWrapEvent: any = { + kind: 1059, // gift wrap + created_at: created, + tags: [["p", receiverPK]], + pubkey: getPublicKey(secretKey), + content: encryptedEvent, + }; + if (expiryDays != null) { + const expiration = created + (expiryDays + 2) * 86400; + giftWrapEvent.tags.push(["expiration", expiration.toString()]); + } + + return finalizeEvent(giftWrapEvent, secretKey); +}; const decryptNIP17MessageNIP07 = async (wrapped: any): Promise => { - const sealedText = await (window as any).nostr.nip44.decrypt( - wrapped.pubkey, wrapped.content); - const sealed = JSON.parse(sealedText); - const rumourText = await (window as any).nostr.nip44.decrypt( - sealed.pubkey, sealed.content); - return JSON.parse(rumourText); -} + const sealedText = await (window as any).nostr.nip44.decrypt( + wrapped.pubkey, + wrapped.content, + ); + const sealed = JSON.parse(sealedText); + const rumourText = await (window as any).nostr.nip44.decrypt( + sealed.pubkey, + sealed.content, + ); + return JSON.parse(rumourText); +}; // Decrypts the provided message, addressed to the user. // If NIP-07 is available, it uses that, otherwise it triggers the signer modal. export const decrypt = async (wrapped: any, user: any): Promise => { - const sk = await ensureSigner(user.pubkey); + const sk = await ensureSigner(user.pubkey); - if (sk === null) { - return decryptNIP17MessageNIP07(wrapped); - } else { - return unwrapEvent(wrapped, sk); - } -} + if (sk === null) { + return decryptNIP17MessageNIP07(wrapped); + } else { + return unwrapEvent(wrapped, sk); + } +}; // Returns the rumours of the messages the provided user has sent or received. // If the events have an expiration timestamp, // that timestamp is added to its rumour object as the property `expiration`. export const getMessageRumours = async (user: any): Promise => { - const preferredRelays = await getPreferredRelays(user.pubkey); - // Always include default relays to catch messages that might be there - const relays = Array.from(new Set([...preferredRelays, ...DM_RELAYS_LIST])); - const wrapped = await pool.querySync( - relays, { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT } - ); + const preferredRelays = await getPreferredRelays(user.pubkey); + // Always include default relays to catch messages that might be there + const relays = Array.from(new Set([...preferredRelays, ...DM_RELAYS_LIST])); + const wrapped = await pool.querySync(relays, { + kinds: [1059], + "#p": [user.pubkey], + limit: DM_FETCH_LIMIT, + }); + + let rumours: any[] = []; + // intentionally decrypting sequentially to avoid having a bunch of popups + for (const event of wrapped) { + if (expired(event)) { + console.log("relay(s) contain expired event", event.id); + continue; + } + + try { + const rumour: any = await decrypt(event, user); + + const expires = expiration(event); + if (expires) { + rumour.expiration = expires; + } - let rumours: any[] = []; - // intentionally decrypting sequentially to avoid having a bunch of popups - for (const event of wrapped) { - if (expired(event)) { - console.log("relay(s) contain expired event", event.id); - continue; - } - - try { - const rumour: any = await decrypt(event, user); - - const expires = expiration(event); - if (expires) { - rumour.expiration = expires; - } - - rumours.push(rumour); - } catch (e) { - console.error('Failed to decrypt event:', event.id, e); - } + rumours.push(rumour); + } catch (e) { + console.error("Failed to decrypt event:", event.id, e); } + } - return rumours; -} + return rumours; +}; // Returns the preferred relays of the user with the provided pubkey, // according to their kind-10050 event. export const getPreferredRelays = async (pubkey: string): Promise => { - if (relayListCache.has(pubkey)) - return relayListCache.get(pubkey); - - const events = await pool.querySync( - DM_RELAYS_LIST, { kinds: [10050], limit: 1, authors: [pubkey] } - ); - - let relays: string[] = []; - for (const event of events) { - for (const tag of event.tags) { - if (tag.length >= 2 && tag[0] == "relay") { - relays.push(tag[1]); - } - } + if (relayListCache.has(pubkey)) return relayListCache.get(pubkey); + + const events = await pool.querySync(DM_RELAYS_LIST, { + kinds: [10050], + limit: 1, + authors: [pubkey], + }); + + let relays: string[] = []; + for (const event of events) { + for (const tag of event.tags) { + if (tag.length >= 2 && tag[0] == "relay") { + relays.push(tag[1]); + } } + } - // Fall back to default DM relays if user hasn't published their preferred relays - if (relays.length === 0) { - relays = DM_RELAYS_LIST; - } + // Fall back to default DM relays if user hasn't published their preferred relays + if (relays.length === 0) { + relays = DM_RELAYS_LIST; + } - relayListCache.set(pubkey, relays); - return relays; -} + relayListCache.set(pubkey, relays); + return relays; +}; // Publish to a single relay with NIP-42 auth support -const publishWithAuth = async (relayUrl: string, event: any, sk: Uint8Array | null): Promise => { - const relay = await Relay.connect(relayUrl); - - const signAuthEvent = async (authEvent: any) => { - if (sk) { - return finalizeEvent(authEvent, sk); - } else if ((window as any).nostr) { - return await (window as any).nostr.signEvent(authEvent); - } - throw new Error('No signing method for auth'); - }; - - try { - return await relay.publish(event); - } catch (e: any) { - // Check if it's an auth error - if (e?.message?.includes('auth-required')) { - // Authenticate and retry - await relay.auth(signAuthEvent); - return await relay.publish(event); - } - throw e; +const publishWithAuth = async ( + relayUrl: string, + event: any, + sk: Uint8Array | null, +): Promise => { + const relay = await Relay.connect(relayUrl); + + const signAuthEvent = async (authEvent: any) => { + if (sk) { + return finalizeEvent(authEvent, sk); + } else if ((window as any).nostr) { + return await (window as any).nostr.signEvent(authEvent); } + throw new Error("No signing method for auth"); + }; + + try { + return await relay.publish(event); + } catch (e: any) { + // Check if it's an auth error + if (e?.message?.includes("auth-required")) { + // Authenticate and retry + await relay.auth(signAuthEvent); + return await relay.publish(event); + } + throw e; + } }; // Store the current sk for use in publish let currentSk: Uint8Array | null = null; -const publishToPreferred = async (event: any, pubkey: string, expiryEnabled: boolean): Promise => { - let preferredRelays = await getPreferredRelays(pubkey); - if (expiryEnabled) { - preferredRelays = await relaysSupporting(preferredRelays, [40]); - } - if (preferredRelays.length === 0) { - throw new Error('No relays available to publish to'); - } - - const promises = preferredRelays.map(url => publishWithAuth(url, event, currentSk)); - try { - return await (Promise as any).any(promises); - } catch (e) { - const results = await Promise.allSettled(promises); - console.error('All relay publishes failed:', results); - throw e; - } -} +const publishToPreferred = async ( + event: any, + pubkey: string, + expiryEnabled: boolean, +): Promise => { + let preferredRelays = await getPreferredRelays(pubkey); + if (expiryEnabled) { + preferredRelays = await relaysSupporting(preferredRelays, [40]); + } + if (preferredRelays.length === 0) { + throw new Error("No relays available to publish to"); + } + + const promises = preferredRelays.map((url) => + publishWithAuth(url, event, currentSk), + ); + try { + return await (Promise as any).any(promises); + } catch (e) { + const results = await Promise.allSettled(promises); + console.error("All relay publishes failed:", results); + throw e; + } +}; // Sends a NIP-17 message from `user` to `recipient`. // If expiryDays is set, messages will expire after that many days. -export const send = async (message: string, user: any, recipient: any, expiryDays?: number) => { - let event1, event2; - const sk = await ensureSigner(user.pubkey); - - // Store sk for auth handler - currentSk = sk; - - if (sk === null) { - // Use NIP-07 extension - event1 = await createNIP17MessageNIP07( - message, user.pubkey, recipient.pubkey, recipient.pubkey, expiryDays); - event2 = await createNIP17MessageNIP07( - message, user.pubkey, recipient.pubkey, user.pubkey, expiryDays); - } else { - event1 = createNIP17MessageSK( - message, sk, recipient.pubkey, recipient.pubkey, expiryDays); - event2 = createNIP17MessageSK( - message, sk, recipient.pubkey, user.pubkey, expiryDays); - } +export const send = async ( + message: string, + user: any, + recipient: any, + expiryDays?: number, +) => { + let event1, event2; + const sk = await ensureSigner(user.pubkey); + + // Store sk for auth handler + currentSk = sk; + + if (sk === null) { + // Use NIP-07 extension + event1 = await createNIP17MessageNIP07( + message, + user.pubkey, + recipient.pubkey, + recipient.pubkey, + expiryDays, + ); + event2 = await createNIP17MessageNIP07( + message, + user.pubkey, + recipient.pubkey, + user.pubkey, + expiryDays, + ); + } else { + event1 = createNIP17MessageSK( + message, + sk, + recipient.pubkey, + recipient.pubkey, + expiryDays, + ); + event2 = createNIP17MessageSK( + message, + sk, + recipient.pubkey, + user.pubkey, + expiryDays, + ); + } - const p1 = publishToPreferred(event1, recipient.pubkey, expiryDays != null); - const p2 = publishToPreferred(event2, user.pubkey, expiryDays != null); - return Promise.all([p1, p2]); -} + const p1 = publishToPreferred(event1, recipient.pubkey, expiryDays != null); + const p2 = publishToPreferred(event2, user.pubkey, expiryDays != null); + return Promise.all([p1, p2]); +}; diff --git a/src/lib/nip40.ts b/src/lib/nip40.ts index 030fe0df..760a6680 100644 --- a/src/lib/nip40.ts +++ b/src/lib/nip40.ts @@ -1,16 +1,16 @@ // Returns true if the message has an expiration in the past, false otherwise. export const expired = (event: object): boolean => { - const expiryTime = expiration(event); - return expiryTime !== null && Date.now() / 1000 >= expiryTime; -} + const expiryTime = expiration(event); + return expiryTime !== null && Date.now() / 1000 >= expiryTime; +}; // Returns the provided event's expiration time, as a Unix ms integer. // If it has none, returns null. export const expiration = (event: object): number | null => { - for (const tag of event.tags) { - if (tag[0] == "expiration") { - return parseInt(tag[1]); - } + for (const tag of event.tags) { + if (tag[0] == "expiration") { + return parseInt(tag[1]); } - return null; -} + } + return null; +}; diff --git a/src/lib/nip44.ts b/src/lib/nip44.ts index bd7efdfc..e05fc685 100644 --- a/src/lib/nip44.ts +++ b/src/lib/nip44.ts @@ -1,135 +1,163 @@ -// Copied from this public-domain code: -// https://github.com/paulmillr/nip44/blob/main/javascript/index.ts -// Modified slightly to deal with a dependency issue - -import { chacha20 } from '@noble/ciphers/chacha.js'; -import { equalBytes } from '@noble/ciphers/utils.js'; -import { secp256k1 } from '@noble/curves/secp256k1.js'; -import { extract as hkdf_extract, expand as hkdf_expand } from '@noble/hashes/hkdf.js'; -import { hmac } from '@noble/hashes/hmac.js'; -import { sha256 } from '@noble/hashes/sha2.js'; -import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils.js'; -import { base64 } from '@scure/base'; - -declare const TextDecoder: any; - -const decoder = new TextDecoder(); -export const u = { - minPlaintextSize: 0x0001, // 1b msg => padded to 32b - maxPlaintextSize: 0xffff, // 65535 (64kb-1) => padded to 64kb - - utf8Encode: utf8ToBytes, - utf8Decode(bytes: Uint8Array) { - return decoder.decode(bytes); - }, - - getConversationKey(privkeyA: string, pubkeyB: string): Uint8Array { - const sharedX = secp256k1.getSharedSecret(privkeyA, '02' + pubkeyB).subarray(1, 33); - return hkdf_extract(sha256, sharedX, 'nip44-v2'); - }, - - getMessageKeys(conversationKey: Uint8Array, nonce: Uint8Array) { - // ensureBytes(conversationKey, 32); - // ensureBytes(nonce, 32); - const keys = hkdf_expand(sha256, conversationKey, nonce, 76); - return { - chacha_key: keys.subarray(0, 32), - chacha_nonce: keys.subarray(32, 44), - hmac_key: keys.subarray(44, 76), - }; - }, - - calcPaddedLen(len: number): number { - if (!Number.isSafeInteger(len) || len < 1) throw new Error('expected positive integer'); - if (len <= 32) return 32; - const nextPower = 1 << (Math.floor(Math.log2(len - 1)) + 1); - const chunk = nextPower <= 256 ? 32 : nextPower / 8; - return chunk * (Math.floor((len - 1) / chunk) + 1); - }, - - writeU16BE(num: number) { - if (!Number.isSafeInteger(num) || num < u.minPlaintextSize || num > u.maxPlaintextSize) - throw new Error('invalid plaintext size: must be between 1 and 65535 bytes'); - const arr = new Uint8Array(2); - new DataView(arr.buffer).setUint16(0, num, false); - return arr; - }, - - pad(plaintext: string): Uint8Array { - const unpadded = u.utf8Encode(plaintext); - const unpaddedLen = unpadded.length; - const prefix = u.writeU16BE(unpaddedLen); - const suffix = new Uint8Array(u.calcPaddedLen(unpaddedLen) - unpaddedLen); - return concatBytes(prefix, unpadded, suffix); - }, - - unpad(padded: Uint8Array): string { - const unpaddedLen = new DataView(padded.buffer).getUint16(0); - const unpadded = padded.subarray(2, 2 + unpaddedLen); - if ( - unpaddedLen < u.minPlaintextSize || - unpaddedLen > u.maxPlaintextSize || - unpadded.length !== unpaddedLen || - padded.length !== 2 + u.calcPaddedLen(unpaddedLen) - ) - throw new Error('invalid padding'); - return u.utf8Decode(unpadded); - }, - - hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array) { - if (aad.length !== 32) throw new Error('AAD associated data must be 32 bytes'); - const combined = concatBytes(aad, message); - return hmac(sha256, key, combined); - }, - - // metadata: always 65b (version: 1b, nonce: 32b, max: 32b) - // plaintext: 1b to 0xffff - // padded plaintext: 32b to 0xffff - // ciphertext: 32b+2 to 0xffff+2 - // raw payload: 99 (65+32+2) to 65603 (65+0xffff+2) - // compressed payload (base64): 132b to 87472b - decodePayload(payload: string) { - if (typeof payload !== 'string') throw new Error('payload must be a valid string'); - const plen = payload.length; - if (plen < 132 || plen > 87472) throw new Error('invalid payload length: ' + plen); - if (payload[0] === '#') throw new Error('unknown encryption version'); - let data: Uint8Array; - try { - data = base64.decode(payload); - } catch (error) { - throw new Error('invalid base64: ' + (error as any).message); - } - const dlen = data.length; - if (dlen < 99 || dlen > 65603) throw new Error('invalid data length: ' + dlen); - const vers = data[0]; - if (vers !== 2) throw new Error('unknown encryption version ' + vers); - return { - nonce: data.subarray(1, 33), - ciphertext: data.subarray(33, -32), - mac: data.subarray(-32), - }; - }, -}; - -export function encrypt(plaintext: string, conversationKey: Uint8Array, nonce = randomBytes(32)): string { - const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(conversationKey, nonce); - const padded = u.pad(plaintext); - const ciphertext = chacha20(chacha_key, chacha_nonce, padded); - const mac = u.hmacAad(hmac_key, ciphertext, nonce); - return base64.encode(concatBytes(new Uint8Array([2]), nonce, ciphertext, mac)); -} - -export function decrypt(payload: string, conversationKey: Uint8Array): string { - const { nonce, ciphertext, mac } = u.decodePayload(payload); - const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(conversationKey, nonce); - const calculatedMac = u.hmacAad(hmac_key, ciphertext, nonce); - if (!equalBytes(calculatedMac, mac)) throw new Error('invalid MAC'); - const padded = chacha20(chacha_key, chacha_nonce, ciphertext); - return u.unpad(padded); -} - -export const v2 = { - utils: u, - encrypt, - decrypt, -}; +// Copied from this public-domain code: +// https://github.com/paulmillr/nip44/blob/main/javascript/index.ts +// Modified slightly to deal with a dependency issue + +import { chacha20 } from "@noble/ciphers/chacha.js"; +import { equalBytes } from "@noble/ciphers/utils.js"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { + extract as hkdf_extract, + expand as hkdf_expand, +} from "@noble/hashes/hkdf.js"; +import { hmac } from "@noble/hashes/hmac.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { concatBytes, randomBytes, utf8ToBytes } from "@noble/hashes/utils.js"; +import { base64 } from "@scure/base"; + +declare const TextDecoder: any; + +const decoder = new TextDecoder(); +export const u = { + minPlaintextSize: 0x0001, // 1b msg => padded to 32b + maxPlaintextSize: 0xffff, // 65535 (64kb-1) => padded to 64kb + + utf8Encode: utf8ToBytes, + utf8Decode(bytes: Uint8Array) { + return decoder.decode(bytes); + }, + + getConversationKey(privkeyA: string, pubkeyB: string): Uint8Array { + const sharedX = secp256k1 + .getSharedSecret(privkeyA, "02" + pubkeyB) + .subarray(1, 33); + return hkdf_extract(sha256, sharedX, "nip44-v2"); + }, + + getMessageKeys(conversationKey: Uint8Array, nonce: Uint8Array) { + // ensureBytes(conversationKey, 32); + // ensureBytes(nonce, 32); + const keys = hkdf_expand(sha256, conversationKey, nonce, 76); + return { + chacha_key: keys.subarray(0, 32), + chacha_nonce: keys.subarray(32, 44), + hmac_key: keys.subarray(44, 76), + }; + }, + + calcPaddedLen(len: number): number { + if (!Number.isSafeInteger(len) || len < 1) + throw new Error("expected positive integer"); + if (len <= 32) return 32; + const nextPower = 1 << (Math.floor(Math.log2(len - 1)) + 1); + const chunk = nextPower <= 256 ? 32 : nextPower / 8; + return chunk * (Math.floor((len - 1) / chunk) + 1); + }, + + writeU16BE(num: number) { + if ( + !Number.isSafeInteger(num) || + num < u.minPlaintextSize || + num > u.maxPlaintextSize + ) + throw new Error( + "invalid plaintext size: must be between 1 and 65535 bytes", + ); + const arr = new Uint8Array(2); + new DataView(arr.buffer).setUint16(0, num, false); + return arr; + }, + + pad(plaintext: string): Uint8Array { + const unpadded = u.utf8Encode(plaintext); + const unpaddedLen = unpadded.length; + const prefix = u.writeU16BE(unpaddedLen); + const suffix = new Uint8Array(u.calcPaddedLen(unpaddedLen) - unpaddedLen); + return concatBytes(prefix, unpadded, suffix); + }, + + unpad(padded: Uint8Array): string { + const unpaddedLen = new DataView(padded.buffer).getUint16(0); + const unpadded = padded.subarray(2, 2 + unpaddedLen); + if ( + unpaddedLen < u.minPlaintextSize || + unpaddedLen > u.maxPlaintextSize || + unpadded.length !== unpaddedLen || + padded.length !== 2 + u.calcPaddedLen(unpaddedLen) + ) + throw new Error("invalid padding"); + return u.utf8Decode(unpadded); + }, + + hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array) { + if (aad.length !== 32) + throw new Error("AAD associated data must be 32 bytes"); + const combined = concatBytes(aad, message); + return hmac(sha256, key, combined); + }, + + // metadata: always 65b (version: 1b, nonce: 32b, max: 32b) + // plaintext: 1b to 0xffff + // padded plaintext: 32b to 0xffff + // ciphertext: 32b+2 to 0xffff+2 + // raw payload: 99 (65+32+2) to 65603 (65+0xffff+2) + // compressed payload (base64): 132b to 87472b + decodePayload(payload: string) { + if (typeof payload !== "string") + throw new Error("payload must be a valid string"); + const plen = payload.length; + if (plen < 132 || plen > 87472) + throw new Error("invalid payload length: " + plen); + if (payload[0] === "#") throw new Error("unknown encryption version"); + let data: Uint8Array; + try { + data = base64.decode(payload); + } catch (error) { + throw new Error("invalid base64: " + (error as any).message); + } + const dlen = data.length; + if (dlen < 99 || dlen > 65603) + throw new Error("invalid data length: " + dlen); + const vers = data[0]; + if (vers !== 2) throw new Error("unknown encryption version " + vers); + return { + nonce: data.subarray(1, 33), + ciphertext: data.subarray(33, -32), + mac: data.subarray(-32), + }; + }, +}; + +export function encrypt( + plaintext: string, + conversationKey: Uint8Array, + nonce = randomBytes(32), +): string { + const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys( + conversationKey, + nonce, + ); + const padded = u.pad(plaintext); + const ciphertext = chacha20(chacha_key, chacha_nonce, padded); + const mac = u.hmacAad(hmac_key, ciphertext, nonce); + return base64.encode( + concatBytes(new Uint8Array([2]), nonce, ciphertext, mac), + ); +} + +export function decrypt(payload: string, conversationKey: Uint8Array): string { + const { nonce, ciphertext, mac } = u.decodePayload(payload); + const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys( + conversationKey, + nonce, + ); + const calculatedMac = u.hmacAad(hmac_key, ciphertext, nonce); + if (!equalBytes(calculatedMac, mac)) throw new Error("invalid MAC"); + const padded = chacha20(chacha_key, chacha_nonce, ciphertext); + return u.unpad(padded); +} + +export const v2 = { + utils: u, + encrypt, + decrypt, +}; diff --git a/src/lib/nip51.ts b/src/lib/nip51.ts index 0210bf8b..7560dd3d 100644 --- a/src/lib/nip51.ts +++ b/src/lib/nip51.ts @@ -1,70 +1,81 @@ -import { SimplePool } from 'nostr-tools/pool'; -import { signEvent, encrypt, decrypt } from '$lib/nip07'; +import { SimplePool } from "nostr-tools/pool"; +import { signEvent, encrypt, decrypt } from "$lib/nip07"; const pool = new SimplePool(); -import { PUBLIC_DM_RELAYS } from '$env/static/public'; -const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); +import { PUBLIC_DM_RELAYS } from "$env/static/public"; +const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(","); // Mute the provided pubkey, as the provided user. export const mute = async (user: any, pubkey: string, hide: boolean) => { - let { shown, hidden } = await getMuteLists(user); - if (hide) { - hidden.push(["p", pubkey]); - } else { - shown.push(["p", pubkey]); - } - await publishMuteList(user, shown, hidden); -} + let { shown, hidden } = await getMuteLists(user); + if (hide) { + hidden.push(["p", pubkey]); + } else { + shown.push(["p", pubkey]); + } + await publishMuteList(user, shown, hidden); +}; const tagMatches = (tag: string[], pubkey: string): boolean => - tag.length >= 2 && tag[0] === "p" && tag[1] === pubkey; + tag.length >= 2 && tag[0] === "p" && tag[1] === pubkey; // Unmute the provided pubkey, as the provided user. export const unmute = async (user: any, pubkey: string) => { - let { shown, hidden } = await getMuteLists(user); - shown = shown.filter((tag: string[]) => !tagMatches(tag, pubkey)); - hidden = hidden.filter((tag: string[]) => !tagMatches(tag, pubkey)); - await publishMuteList(user, shown, hidden); -} + let { shown, hidden } = await getMuteLists(user); + shown = shown.filter((tag: string[]) => !tagMatches(tag, pubkey)); + hidden = hidden.filter((tag: string[]) => !tagMatches(tag, pubkey)); + await publishMuteList(user, shown, hidden); +}; // Returns a set of the provided user's muted accounts. export const mutedAccounts = async (user: any): Promise> => { - const { shown, hidden } = await getMuteLists(user); - let muted = new Set(); - for (const tag of shown) { - if (tag.length >= 2 && tag[0] === "p") { - muted.add(tag[1]); - } + const { shown, hidden } = await getMuteLists(user); + let muted = new Set(); + for (const tag of shown) { + if (tag.length >= 2 && tag[0] === "p") { + muted.add(tag[1]); } - for (const tag of hidden) { - if (tag.length >= 2 && tag[0] === "p") { - muted.add(tag[1]); - } + } + for (const tag of hidden) { + if (tag.length >= 2 && tag[0] === "p") { + muted.add(tag[1]); } - return muted; -} + } + return muted; +}; const getMuteLists = async (user: any): Promise => { - const listEvents = await pool.querySync( - DM_RELAYS_LIST, { kinds: [10000], "authors": [user.pubkey], limit: 1 } - ); - if (listEvents.length == 0) { - return { shown: [], hidden: [] }; - } + const listEvents = await pool.querySync(DM_RELAYS_LIST, { + kinds: [10000], + authors: [user.pubkey], + limit: 1, + }); + if (listEvents.length == 0) { + return { shown: [], hidden: [] }; + } - return { - shown: listEvents[0].tags, - hidden: listEvents[0].content == "" ? [] : - await decrypt(listEvents[0].content, user, user.pubkey) - }; -} + return { + shown: listEvents[0].tags, + hidden: + listEvents[0].content == "" + ? [] + : await decrypt(listEvents[0].content, user, user.pubkey), + }; +}; -const publishMuteList = async (user: any, shown: string[], hidden: string[]) => { - const event = await signEvent({ - kind: 10000, - created_at: Math.floor(Date.now() / 1000), - tags: shown, - content: await encrypt(hidden, user, user.pubkey) - }, user); - return Promise.any(pool.publish(DM_RELAYS_LIST, event)); -} +const publishMuteList = async ( + user: any, + shown: string[], + hidden: string[], +) => { + const event = await signEvent( + { + kind: 10000, + created_at: Math.floor(Date.now() / 1000), + tags: shown, + content: await encrypt(hidden, user, user.pubkey), + }, + user, + ); + return Promise.any(pool.publish(DM_RELAYS_LIST, event)); +}; diff --git a/src/lib/nostr.ts b/src/lib/nostr.ts index dd283987..50df6408 100644 --- a/src/lib/nostr.ts +++ b/src/lib/nostr.ts @@ -1,239 +1,239 @@ import { Buffer } from "buffer"; import { browser } from "$app/environment"; import { - decrypted, - eventToSign, - password as pw, - passwordPrompt, - signer as $signer, + decrypted, + eventToSign, + password as pw, + passwordPrompt, + signer as $signer, } from "$lib/store"; import { post, wait } from "$lib/utils"; import { bytesToHex } from "@noble/hashes/utils.js"; import type { EventTemplate } from "nostr-tools"; import { get } from "svelte/store"; -let nostrToolsPromise: - | Promise - | undefined; +let nostrToolsPromise: Promise | undefined; let nip44Promise: Promise | undefined; let relayPromise: Promise | undefined; let nip49Promise: Promise | undefined; -const loadNostrTools = () => - (nostrToolsPromise ||= import("nostr-tools")); +const loadNostrTools = () => (nostrToolsPromise ||= import("nostr-tools")); const loadNip44 = () => (nip44Promise ||= import("nostr-tools/nip44")); const loadRelay = () => (relayPromise ||= import("nostr-tools/relay")); const loadNip49 = () => (nip49Promise ||= import("nostr-tools/nip49")); type User = { - [key: string]: any; + [key: string]: any; }; type EncryptParams = { - message: string; - recipient: string; - user: User; + message: string; + recipient: string; + user: User; }; export const encrypt = async ({ message, recipient, user }: EncryptParams) => { - const sk = await getPrivateKey(user); - const { nip04 } = await loadNostrTools(); - return nip04.encrypt(sk, recipient, message); + const sk = await getPrivateKey(user); + const { nip04 } = await loadNostrTools(); + return nip04.encrypt(sk, recipient, message); }; export const decrypt = async ({ event, user }: { event: any; user: any }) => { - const cache = get(decrypted); - try { - let { content, pubkey, id } = event; - if (cache[id]) return cache[id]; - if (pubkey === user.pubkey) pubkey = event.tags[0][1]; - - const { nip04 } = await loadNostrTools(); - const message = await nip04.decrypt( - await getPrivateKey(user), - pubkey, - content, - ); - - cache[id] = Buffer.from(message).toString("utf8"); - decrypted.set(cache); - - return cache[id]; - } catch (e) { - // console.log(e); - } + const cache = get(decrypted); + try { + let { content, pubkey, id } = event; + if (cache[id]) return cache[id]; + if (pubkey === user.pubkey) pubkey = event.tags[0][1]; + + const { nip04 } = await loadNostrTools(); + const message = await nip04.decrypt( + await getPrivateKey(user), + pubkey, + content, + ); + + cache[id] = Buffer.from(message).toString("utf8"); + decrypted.set(cache); + + return cache[id]; + } catch { + // console.log(e); + } }; export const getPrivateKey = async (user: User): Promise => { - let k; - if (browser) { - k = localStorage.getItem("nsec"); - if (k) { - const { nip19 } = await loadNostrTools(); - return nip19.decode(k).data as Uint8Array; - } - } - - const { nsec } = user; - - if (nsec) { - const { decrypt: nip49decrypt } = await loadNip49(); - k = nip49decrypt(nsec, await getPassword()); - } else { - throw new Error("nsec not available"); - } - - const { nip19 } = await loadNostrTools(); - localStorage.setItem("nsec", nip19.nsecEncode(k)); - return k; + let k; + if (browser) { + k = localStorage.getItem("nsec"); + if (k) { + const { nip19 } = await loadNostrTools(); + return nip19.decode(k).data as Uint8Array; + } + } + + const { nsec } = user; + + if (nsec) { + const { decrypt: nip49decrypt } = await loadNip49(); + k = nip49decrypt(nsec, await getPassword()); + } else { + throw new Error("nsec not available"); + } + + const { nip19 } = await loadNostrTools(); + localStorage.setItem("nsec", nip19.nsecEncode(k)); + return k; }; const decodeNsec = async (nsec: string): Promise => { - const { nip19 } = await loadNostrTools(); - const { type, data } = nip19.decode(nsec); - if (type === "nsec" && data instanceof Uint8Array) return data; - throw new Error("invalid nsec"); + const { nip19 } = await loadNostrTools(); + const { type, data } = nip19.decode(nsec); + if (type === "nsec" && data instanceof Uint8Array) return data; + throw new Error("invalid nsec"); }; export const getNsec = async (user: User) => { - const { nip19 } = await loadNostrTools(); - return nip19.nsecEncode(await getPrivateKey(user)); + const { nip19 } = await loadNostrTools(); + return nip19.nsecEncode(await getPrivateKey(user)); }; export const setNsec = async (user: User, nsec: string) => { - const { getPublicKey } = await loadNostrTools(); - user.pubkey = getPublicKey(await decodeNsec(nsec)); - user.nsec = await encryptNsec(nsec); + const { getPublicKey } = await loadNostrTools(); + user.pubkey = getPublicKey(await decodeNsec(nsec)); + user.nsec = await encryptNsec(nsec); }; export const encryptNsec = async (nsec: string) => { - const { encrypt: nip49encrypt } = await loadNip49(); - const d = await decodeNsec(nsec); - return nip49encrypt(d, await getPassword()); + const { encrypt: nip49encrypt } = await loadNip49(); + const d = await decodeNsec(nsec); + return nip49encrypt(d, await getPassword()); }; export const sign = async (event: any, user?: any) => { - if (user?.nsec && !get($signer)?.ready) { - const sk = await getPrivateKey(user); - const { getPublicKey } = await loadNostrTools(); - - $signer.set({ - method: "nsec", - ready: true, - params: { sk: bytesToHex(sk), pk: getPublicKey(sk) }, - }); - } - - eventToSign.set(event); - let unsubscribe; - const { method, params } = await new Promise((r, j) => { - unsubscribe = $signer.subscribe((v) => { - if (v === "cancel") { - j("cancelled"); - $signer.set(undefined); - } - v?.ready && r(v); - }); - }); - await unsubscribe(); - const signedEvent = await signingMethods[method](event, params); - eventToSign.set(signedEvent); - setTimeout(() => eventToSign.set(null), 1000); - return signedEvent; + if (user?.nsec && !get($signer)?.ready) { + const sk = await getPrivateKey(user); + const { getPublicKey } = await loadNostrTools(); + + $signer.set({ + method: "nsec", + ready: true, + params: { sk: bytesToHex(sk), pk: getPublicKey(sk) }, + }); + } + + eventToSign.set(event); + let unsubscribe; + const { method, params } = await new Promise((r, j) => { + unsubscribe = $signer.subscribe((v) => { + if (v === "cancel") { + j("cancelled"); + $signer.set(undefined); + } + v?.ready && r(v); + }); + }); + await unsubscribe(); + const signedEvent = await signingMethods[method](event, params); + eventToSign.set(signedEvent); + setTimeout(() => eventToSign.set(null), 1000); + return signedEvent; }; export const send = (event: EventTemplate) => { - return post("/events", { event }); + return post("/events", { event }); }; const getPassword = async (): Promise => { - if (!get(pw)) passwordPrompt.set(true); - await wait(() => !!get(pw)); - return get(pw) || ""; + if (!get(pw)) passwordPrompt.set(true); + await wait(() => !!get(pw)); + return get(pw) || ""; }; export const nostrConnectRelay = "wss://relay.nsec.app"; -const signingMethods: Record Promise> = { - async connect(event: any, { sk, pk, pubkey }: any) { - const id = crypto.randomUUID(); - const signEvent = { - id, - method: "sign_event", - params: [JSON.stringify(event)], - }; - - const { nip04, finalizeEvent } = await loadNostrTools(); - const content = await nip04.encrypt(sk, pk, JSON.stringify(signEvent)); - - const signedSignEvent = finalizeEvent( - { - created_at: Math.round(Date.now() / 1000), - kind: 24133, - pubkey, - content, - tags: [["p", pk]], - }, - sk, - ); - - const { Relay } = await loadRelay(); - const relay = await Relay.connect(nostrConnectRelay); - relay.publish(signedSignEvent); - - return new Promise((resolve) => { - relay.subscribe([{ kinds: [24133], "#p": [pubkey] }], { - async onevent(event) { - pk = event.pubkey; - try { - let response; - try { - const { nip04 } = await loadNostrTools(); - response = JSON.parse(await nip04.decrypt(sk, pk, event.content)); - } catch (e) { - const { getConversationKey, decrypt: nip44decrypt } = - await loadNip44(); - const ck = await getConversationKey(sk, pk); - response = JSON.parse(await nip44decrypt(event.content, ck)); - } - - if (response.id === id) resolve(JSON.parse(response.result)); - } catch (e) { - console.log("failed to parse nostr connect event", e); - } - }, - }); - }); - }, - - async nsec(event: any, { sk }: any) { - const { finalizeEvent } = await loadNostrTools(); - return finalizeEvent(event, sk); - }, - - async extension(event: any) { - return window.nostr!.signEvent(event); - }, - - async signer(event: any, { pubkey, sig }: any) { - event.pubkey = pubkey; - const { getEventHash } = await loadNostrTools(); - const signedEvent = { - ...event, - id: getEventHash(event), - sig, - }; - return signedEvent; - }, +const signingMethods: Record< + string, + (event: any, params: any) => Promise +> = { + async connect(event: any, { sk, pk, pubkey }: any) { + const id = crypto.randomUUID(); + const signEvent = { + id, + method: "sign_event", + params: [JSON.stringify(event)], + }; + + const { nip04, finalizeEvent } = await loadNostrTools(); + const content = await nip04.encrypt(sk, pk, JSON.stringify(signEvent)); + + const signedSignEvent = finalizeEvent( + { + created_at: Math.round(Date.now() / 1000), + kind: 24133, + pubkey, + content, + tags: [["p", pk]], + }, + sk, + ); + + const { Relay } = await loadRelay(); + const relay = await Relay.connect(nostrConnectRelay); + relay.publish(signedSignEvent); + + return new Promise((resolve) => { + relay.subscribe([{ kinds: [24133], "#p": [pubkey] }], { + async onevent(event) { + pk = event.pubkey; + try { + let response; + try { + const { nip04 } = await loadNostrTools(); + response = JSON.parse(await nip04.decrypt(sk, pk, event.content)); + } catch { + const { getConversationKey, decrypt: nip44decrypt } = + await loadNip44(); + const ck = await getConversationKey(sk, pk); + response = JSON.parse(await nip44decrypt(event.content, ck)); + } + + if (response.id === id) resolve(JSON.parse(response.result)); + } catch (e) { + console.log("failed to parse nostr connect event", e); + } + }, + }); + }); + }, + + async nsec(event: any, { sk }: any) { + const { finalizeEvent } = await loadNostrTools(); + return finalizeEvent(event, sk); + }, + + async extension(event: any) { + return window.nostr!.signEvent(event); + }, + + async signer(event: any, { pubkey, sig }: any) { + event.pubkey = pubkey; + const { getEventHash } = await loadNostrTools(); + const signedEvent = { + ...event, + id: getEventHash(event), + sig, + }; + return signedEvent; + }, }; // Returns all the keys mentioned in the provided event's 'p' tags. export const pTagKeys = (event: any): string[] => { - let keys: string[] = []; - for (const tag of event.tags) { - if (tag[0] === "p") { - keys.push(tag[1]); - } - } - return keys; -} + let keys: string[] = []; + for (const tag of event.tags) { + if (tag[0] === "p") { + keys.push(tag[1]); + } + } + return keys; +}; diff --git a/src/lib/parse.ts b/src/lib/parse.ts index 3249b9db..38c32a54 100644 --- a/src/lib/parse.ts +++ b/src/lib/parse.ts @@ -4,137 +4,137 @@ import { auth, get, isLiquid, post, sats } from "$lib/utils"; import { redirect } from "@sveltejs/kit"; let validatePromise: - | Promise - | undefined; + | Promise + | undefined; const loadValidate = () => - (validatePromise ||= import("bitcoin-address-validation")); + (validatePromise ||= import("bitcoin-address-validation")); export default async (s, host, cookies) => { - if (!s) return; - let t = s.trim(); - let amount; - let invoice; - let user; - - if (t.startsWith("ark") || t.startsWith("tark")) - redirect(307, `/send/ark/${t}`); - - if (t.startsWith("http")) redirect(307, t); - if (t.startsWith(host)) redirect(307, `http://${t}`); - - if (t.includes("lightning=")) { - const url = new URL(t); - const params = new URLSearchParams(url.search); - t = params.get("lightning"); - } - - if (t.startsWith("lightning:")) t = t.replace("lightning:", ""); - if (t.endsWith(`@${PUBLIC_DOMAIN}`)) t = t.split("@")[0]; - if (t.includes("@") && t.includes(".")) { - try { - t = await get(`/encode?address=${t}`); - } catch (e) {} - } - - if (t.toLowerCase().startsWith("nostr:")) { - t = t.split(":")[1]; - } - - if (["note", "nevent"].some((p) => t.startsWith(p))) redirect(307, `/e/${t}`); - if (["nprofile", "npub"].some((p) => t.startsWith(p))) redirect(307, `/${t}`); - - // bitcoin - if ( - t.toLowerCase().startsWith("bitcoin:") || - t.toLowerCase().startsWith("liquidnetwork:") - ) { - ({ - address: t, - options: { amount }, - } = decode(t)); - } - - const { validate } = await loadValidate(); - if (validate(t) || isLiquid(t)) { - let r = `/send/bitcoin/${t}`; - if (amount) r += `/${Math.round(amount * sats)}`; - redirect(307, r); - } - - // lightning - if (t.toLowerCase().startsWith("lightning:")) - t = t.toLowerCase().replace("lightning:", ""); - if (t.toLowerCase().startsWith("lnurl")) redirect(307, `/ln/${t}`); - if (t.includes(":")) t = t.split(":")[1]; - - if (t.toLowerCase().startsWith("lnr")) { - const { iid } = await post("/sendinvoice", { invreq: t }, auth(cookies)); - invoice = { id: iid }; - } else if (t.toLowerCase().startsWith("ln")) { - try { - const aid = cookies.get("aid"); - invoice = await get(`/invoice/${t}`); - if (invoice.user.username === "mint") throw new Error("mint payment"); - if (aid && aid !== invoice.aid) throw new Error("cross-account"); - } catch (e) { - if (t.toLowerCase().startsWith("lno")) { - const { offer_amount_msat: a } = await get(`/decode/${t}`); - if (a) ({ invoice: t } = await post("/fetchinvoice", { offer: t })); - } - redirect(307, `/send/lightning/${t}`); - } - } - - if (invoice) redirect(307, `/invoice/${invoice.id}`); - - if (t.match(/^🥜([\uFE00-\uFE0F]|[\uE0100-\uE01EF]+)$/)) { - t = Array.from(t) - .slice(1) - .map((char) => { - const codePoint = char.codePointAt(0); - - // Handle Variation Selectors (VS1-VS16): U+FE00 to U+FE0F - if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) { - const byteValue = codePoint - 0xfe00; // Maps FE00->0, FE01->1, ..., FE0F->15 - return String.fromCharCode(byteValue); - } - - // Handle Variation Selectors Supplement (VS17-VS256): U+E0100 to U+E01EF - if (codePoint >= 0xe0100 && codePoint <= 0xe01ef) { - const byteValue = codePoint - 0xe0100 + 16; // Maps E0100->16, E0101->17, ..., E01EF->255 - return String.fromCharCode(byteValue); - } - }) - .join(""); - } - - if (t.startsWith("cashu")) { - const { id } = await post("/cash", { token: t }); - redirect(307, `/ecash/${id}`); - } - - if (t.startsWith("creq")) redirect(307, `/send/ecash/${t}`); - - // user - try { - user = await get(`/users/${t.split("/")[0]}`); - if (user.anon) user = null; - } catch (e) {} - - if (user) redirect(307, `/pay/${t}`); - - let fund; - if (t.includes("/fund")) redirect(307, t.substring(t.indexOf("/fund"))); - - try { - fund = await get(`/fund/${t}`); - } catch (e) {} - - if (fund) redirect(307, `/send/fund/${t}`); - - try { - invoice ||= await get(`/invoice/${t}`); - } catch (e) {} - - if (invoice) redirect(307, `/invoice/${invoice.id}`); + if (!s) return; + let t = s.trim(); + let amount; + let invoice; + let user; + + if (t.startsWith("ark") || t.startsWith("tark")) + redirect(307, `/send/ark/${t}`); + + if (t.startsWith("http")) redirect(307, t); + if (t.startsWith(host)) redirect(307, `http://${t}`); + + if (t.includes("lightning=")) { + const url = new URL(t); + const params = new URLSearchParams(url.search); + t = params.get("lightning"); + } + + if (t.startsWith("lightning:")) t = t.replace("lightning:", ""); + if (t.endsWith(`@${PUBLIC_DOMAIN}`)) t = t.split("@")[0]; + if (t.includes("@") && t.includes(".")) { + try { + t = await get(`/encode?address=${t}`); + } catch {} + } + + if (t.toLowerCase().startsWith("nostr:")) { + t = t.split(":")[1]; + } + + if (["note", "nevent"].some((p) => t.startsWith(p))) redirect(307, `/e/${t}`); + if (["nprofile", "npub"].some((p) => t.startsWith(p))) redirect(307, `/${t}`); + + // bitcoin + if ( + t.toLowerCase().startsWith("bitcoin:") || + t.toLowerCase().startsWith("liquidnetwork:") + ) { + ({ + address: t, + options: { amount }, + } = decode(t)); + } + + const { validate } = await loadValidate(); + if (validate(t) || isLiquid(t)) { + let r = `/send/bitcoin/${t}`; + if (amount) r += `/${Math.round(amount * sats)}`; + redirect(307, r); + } + + // lightning + if (t.toLowerCase().startsWith("lightning:")) + t = t.toLowerCase().replace("lightning:", ""); + if (t.toLowerCase().startsWith("lnurl")) redirect(307, `/ln/${t}`); + if (t.includes(":")) t = t.split(":")[1]; + + if (t.toLowerCase().startsWith("lnr")) { + const { iid } = await post("/sendinvoice", { invreq: t }, auth(cookies)); + invoice = { id: iid }; + } else if (t.toLowerCase().startsWith("ln")) { + try { + const aid = cookies.get("aid"); + invoice = await get(`/invoice/${t}`); + if (invoice.user.username === "mint") throw new Error("mint payment"); + if (aid && aid !== invoice.aid) throw new Error("cross-account"); + } catch { + if (t.toLowerCase().startsWith("lno")) { + const { offer_amount_msat: a } = await get(`/decode/${t}`); + if (a) ({ invoice: t } = await post("/fetchinvoice", { offer: t })); + } + redirect(307, `/send/lightning/${t}`); + } + } + + if (invoice) redirect(307, `/invoice/${invoice.id}`); + + if (t.match(/^🥜([\uFE00-\uFE0F]|[\uE0100-\uE01EF]+)$/)) { + t = Array.from(t) + .slice(1) + .map((char) => { + const codePoint = char.codePointAt(0); + + // Handle Variation Selectors (VS1-VS16): U+FE00 to U+FE0F + if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) { + const byteValue = codePoint - 0xfe00; // Maps FE00->0, FE01->1, ..., FE0F->15 + return String.fromCharCode(byteValue); + } + + // Handle Variation Selectors Supplement (VS17-VS256): U+E0100 to U+E01EF + if (codePoint >= 0xe0100 && codePoint <= 0xe01ef) { + const byteValue = codePoint - 0xe0100 + 16; // Maps E0100->16, E0101->17, ..., E01EF->255 + return String.fromCharCode(byteValue); + } + }) + .join(""); + } + + if (t.startsWith("cashu")) { + const { id } = await post("/cash", { token: t }); + redirect(307, `/ecash/${id}`); + } + + if (t.startsWith("creq")) redirect(307, `/send/ecash/${t}`); + + // user + try { + user = await get(`/users/${t.split("/")[0]}`); + if (user.anon) user = null; + } catch {} + + if (user) redirect(307, `/pay/${t}`); + + let fund; + if (t.includes("/fund")) redirect(307, t.substring(t.indexOf("/fund"))); + + try { + fund = await get(`/fund/${t}`); + } catch {} + + if (fund) redirect(307, `/send/fund/${t}`); + + try { + invoice ||= await get(`/invoice/${t}`); + } catch {} + + if (invoice) redirect(307, `/invoice/${invoice.id}`); }; diff --git a/src/lib/passwordCache.ts b/src/lib/passwordCache.ts index 4f764698..a67113e3 100644 --- a/src/lib/passwordCache.ts +++ b/src/lib/passwordCache.ts @@ -5,57 +5,62 @@ const PASSWORD_KEY = "walletPassword"; const EXPIRES_KEY = "walletPasswordExpiresAt"; export const rememberForOptions = [ - { label: "Don't remember", ms: 0 }, - { label: "8 hours", ms: 8 * 60 * 60 * 1000 }, - { label: "1 day", ms: 24 * 60 * 60 * 1000 }, - { label: "1 week", ms: 7 * 24 * 60 * 60 * 1000 }, - { label: "1 month", ms: 30 * 24 * 60 * 60 * 1000 }, - { label: "1 year", ms: 365 * 24 * 60 * 60 * 1000 }, + { label: "Don't remember", ms: 0 }, + { label: "8 hours", ms: 8 * 60 * 60 * 1000 }, + { label: "1 day", ms: 24 * 60 * 60 * 1000 }, + { label: "1 week", ms: 7 * 24 * 60 * 60 * 1000 }, + { label: "1 month", ms: 30 * 24 * 60 * 60 * 1000 }, + { label: "1 year", ms: 365 * 24 * 60 * 60 * 1000 }, ]; export const defaultRememberForMs = 30 * 24 * 60 * 60 * 1000; const clearStoredPassword = () => { - if (!browser) return; - localStorage.removeItem(PASSWORD_KEY); - localStorage.removeItem(EXPIRES_KEY); + if (!browser) return; + localStorage.removeItem(PASSWORD_KEY); + localStorage.removeItem(EXPIRES_KEY); }; const loadStoredPassword = () => { - if (!browser) return undefined; - const password = localStorage.getItem(PASSWORD_KEY) || undefined; - if (!password) return undefined; - const expiresAtRaw = localStorage.getItem(EXPIRES_KEY); - const expiresAt = expiresAtRaw ? Number(expiresAtRaw) : undefined; - if (expiresAt && Date.now() > expiresAt) { - clearStoredPassword(); - return undefined; - } - return password; + if (!browser) return undefined; + const password = localStorage.getItem(PASSWORD_KEY) || undefined; + if (!password) return undefined; + const expiresAtRaw = localStorage.getItem(EXPIRES_KEY); + const expiresAt = expiresAtRaw ? Number(expiresAtRaw) : undefined; + if (expiresAt && Date.now() > expiresAt) { + clearStoredPassword(); + return undefined; + } + return password; }; -export const walletPassword = writable(loadStoredPassword()); - -export const rememberWalletPassword = (password: string, durationMs: number) => { - if (!browser) return; - if (!durationMs) { - clearStoredPassword(); - walletPassword.set(undefined); - return; - } - const expiresAt = Date.now() + durationMs; - localStorage.setItem(PASSWORD_KEY, password); - localStorage.setItem(EXPIRES_KEY, String(expiresAt)); - walletPassword.set(password); +export const walletPassword = writable( + loadStoredPassword(), +); + +export const rememberWalletPassword = ( + password: string, + durationMs: number, +) => { + if (!browser) return; + if (!durationMs) { + clearStoredPassword(); + walletPassword.set(undefined); + return; + } + const expiresAt = Date.now() + durationMs; + localStorage.setItem(PASSWORD_KEY, password); + localStorage.setItem(EXPIRES_KEY, String(expiresAt)); + walletPassword.set(password); }; export const forgetWalletPassword = () => { - clearStoredPassword(); - walletPassword.set(undefined); + clearStoredPassword(); + walletPassword.set(undefined); }; export const getRememberedWalletPassword = () => { - const password = loadStoredPassword(); - walletPassword.set(password); - return password; + const password = loadStoredPassword(); + walletPassword.set(password); + return password; }; diff --git a/src/lib/punks.ts b/src/lib/punks.ts index 8b7b53df..e5551062 100644 --- a/src/lib/punks.ts +++ b/src/lib/punks.ts @@ -1,66 +1,66 @@ export default [ - "6e1f1626a9308abd0b7732fecbc7d49366ced46dead63ef3603fb0f4ec7aa53d", - "96ea95735b61c48c6f465db8ca0246a62539f34c3a977be6338770d92461da89", - "8c18c036b257d7b9c5a970a786efa04c320f3cf5afa73fa310a6603744d1ee3a", - "a7a6187e54e83acdb893d456c8b4ad6877e4738ea9e0d47c9b2070bf2c98a35f", - "836d72150b6f3033b6220d15ac9d49457d81041a700d12898bd3b8e737b4d265", - "e3d1c40538a8aed282a430647e3ff16ff94b64b8ed0a7cf427e8b0e2780a9b2a", - "2155224ef35cfb4bbdb06d982b42bb9b93d97b2bc691c22c804e1759a0a04882", - "5f6549ad9dd15dd57c204b1a9b60c68c376e77d7d114cfa13219e6a1a71f901f", - "ba8dd5a7070aee7102d4f8bc83ac497091e2e473a0f448a1c513569167dd0791", - "4f07e1da3685a858970a9f39300564971e97257f5d37b55a6d9882014aecc421", - "01aa9348244c4b65de3df758d49c98076e17a109aef04bc211abc77cf42dc51d", - "080574eb9e0613926e0e5d9b667c132e12965a1b1d47218a0b290a2d55c55d2f", - "08ce4840184f63a779bcd1f91bbcb56a2e8cba75c3ba2dbd290a559a57e4238e", - "27caa38c14a85ad64b4c4469a34e61a3965a1cbc4cad9a85707da374301c830d", - "eccc90741370ba4acc70bc65f4ccc38fddb1327d977c366156018974bc52fe4a", - "3759946d9e875ac2fc5c51ed850686193bb1e0716219e42efd52f3965993b275", - "db5fe28b8ddb9ae5e4f5e0e757ee60bc938bffbdfdd38de807939e212d3c5066", - "3f8f861e663accee51e68b987705ce647a7446b7f118abc94c632e158acbb44f", - "43bab95a047112dca6705506f916a97252363a20da4a4c0a5ab7b676e11083fc", - "d3018e45c66dd485697f738ad191af98267e6255d562496dd06b1a662e0c7a88", - "0ec2e8d07e053d686140857493ece653cde52764a60aa1d25651d81811d44a3d", - "4595bb092682af7177799464364c047fbfa7ede767336734949bce5d085fce15", - "6524936709e0719f9e28bf1caac91f86f323d1a79f10ab65a03fa9ea2e6b3270", - "8ba4736edc68da8e5b21c23af4de1f9131f47778c2a5b2a970a799223a9380b3", - "2aaba839e8546a8cb6d04a81ff5be83ac4e700ca4491607a16ebd60dc4995750", - "5f88f7f579ce50d7bcf60d6e9556f77acf361ba54caf3eb61dc4f2ca9c94c6a2", - "d1ca262272613d7386fecce7f1ad268188940b59ea83fdd1f8d7ab85771e3f43", - "5211890e4128d8d730802cc75ee105867e8878581d5e72ffc98a0d60027a5398", - "c1dececcbd7b623bfaa726392512e6ea110db93a0649dd01bf9e559318514f7c", - "43ce93c9835e983403a01329918d184832c5516e1b0ea4e0878b4c4def042777", - "c84724a398bda8d20436fb0663d059c196628fa71192bb720fa5adf6137bed9b", - "f307620a90a36045fa550316d61be4755c1f6c7f3fc81c95fc4d590ac568b2c4", - "78ae0118856a1f17c13f292628fcf3dccc150c7278578caaceb9f35ca8bc9f55", - "9d9a9a40c490f05d7dbafde8bf60d68aab7dcb79a7976eef62d1448a4549816b", - "f6017bec50563dbbb24b979214ae225f96b01dea5b090b174038ff2842a1c6b1", - "6dbec748ea87e959390b0df429c005fccffe9e672864f555c14c7a407b940870", - "11701e76233395ebdba8d1d50334d1a0cc3a158f3452260c9a7b9a067b78d753", - "7354f3829dd05c838aa16d66b00316f7994797927a4d3b594c8af04a2b4ae86a", - "14d254333c6240fbdacbbbe261f7ae15be2e83bffcf084f4f59f459bae0a4571", - "7849ac0f31056265efb0110038f86e61effe17b7db836377d88433b73312e65a", - "9018be885177c675baf9b36978428d841ca30a9185e11de1a31238a08e3ec1af", - "777c5715de7423597fea6c03c496ea46414f0508cd96e119261f002965beb805", - "b742b7509a97c2b346ea762f0510e8475b30dc988417f0f146ae00a800c6dd56", - "7af3029a826e6dfba23ad502901a3632ef4ee047bd45d618b1f106717b0af7ea", - "82390020b2bca461204cc49b20ee6fec1b93dad476726f377785d04040a4d6c3", - "6bc2990699be7f610c9f6a49bc7a19b034040727bc3de63fee89a1573fe00673", - "50c7c2e8e8c7f4aabffb195187a1ac1105f5712859f02d491d794ba2d8498e08", - "5d1d3769cd141603ce907df5dca159e389ca4d75338eddbe1b7db1c0766cbfd6", - "4d8ad7a761d068b7ef9b058fb4ba924b276e7a45a3ad47a79b767e08e821e038", - "d5b5b2167602f7277cdac76cfda8bdad0befcb8ff12df495dfabf40c91d1120c", - "6f2618272bb0164247e9b0f40b19f020c1c9253510dc658a3cc078d079ef01bf", - "fd502d5007bfc62a91eb7a34c1ae23b9de4a20cf5e1bbd86dadc421d7083099c", - "914813583b901044970b9c7d6dcdfde4344339325f5988d0dd681d377f1243fb", - "85cb7cf0245613d08b3027f879f58a6db645bf0db107242beb856b340ee49ee8", - "d6dbb18d5df5e92c8c74ee59aa86d19dff25a5d0f9d030e84285a686f86804d7", - "65309f82cd9f9926a578d09e1ad13909f68a06d02b853760731a4225f7aecef4", - "433519d13f2b64fe82a90cd054797d4779507fea93c1054bd51c35ba846a0f7f", - "62a55d4d6e4b6f992a1c1840f041585c957affcdbf875202cf9e1db9def05aeb", - "e19eb1eeabdeffa04ccd792eb09776690cf2121887b26a7c5d20d66e3e8fd0fb", - "a80951fc593035abb2dda045a2076d29a30f5cc470e0a20db39c3ec588ed61d8", - "4f1b39906714266a7a1936bfa8e08c5d891285dd2b188b4bcd4afb0107671004", - "f46310b2e60727af071d09e759814f52f933688a0ff0995c7da373b381e70005", - "2cc94821f56d21918ba4f2770d97f280e06fd99f4199167e2f4ddc780f404c8a", - "698a2ca7a09e88fa6594801da037f4dbce6ae841d1d33736aebcf8f6fe104e01", + "6e1f1626a9308abd0b7732fecbc7d49366ced46dead63ef3603fb0f4ec7aa53d", + "96ea95735b61c48c6f465db8ca0246a62539f34c3a977be6338770d92461da89", + "8c18c036b257d7b9c5a970a786efa04c320f3cf5afa73fa310a6603744d1ee3a", + "a7a6187e54e83acdb893d456c8b4ad6877e4738ea9e0d47c9b2070bf2c98a35f", + "836d72150b6f3033b6220d15ac9d49457d81041a700d12898bd3b8e737b4d265", + "e3d1c40538a8aed282a430647e3ff16ff94b64b8ed0a7cf427e8b0e2780a9b2a", + "2155224ef35cfb4bbdb06d982b42bb9b93d97b2bc691c22c804e1759a0a04882", + "5f6549ad9dd15dd57c204b1a9b60c68c376e77d7d114cfa13219e6a1a71f901f", + "ba8dd5a7070aee7102d4f8bc83ac497091e2e473a0f448a1c513569167dd0791", + "4f07e1da3685a858970a9f39300564971e97257f5d37b55a6d9882014aecc421", + "01aa9348244c4b65de3df758d49c98076e17a109aef04bc211abc77cf42dc51d", + "080574eb9e0613926e0e5d9b667c132e12965a1b1d47218a0b290a2d55c55d2f", + "08ce4840184f63a779bcd1f91bbcb56a2e8cba75c3ba2dbd290a559a57e4238e", + "27caa38c14a85ad64b4c4469a34e61a3965a1cbc4cad9a85707da374301c830d", + "eccc90741370ba4acc70bc65f4ccc38fddb1327d977c366156018974bc52fe4a", + "3759946d9e875ac2fc5c51ed850686193bb1e0716219e42efd52f3965993b275", + "db5fe28b8ddb9ae5e4f5e0e757ee60bc938bffbdfdd38de807939e212d3c5066", + "3f8f861e663accee51e68b987705ce647a7446b7f118abc94c632e158acbb44f", + "43bab95a047112dca6705506f916a97252363a20da4a4c0a5ab7b676e11083fc", + "d3018e45c66dd485697f738ad191af98267e6255d562496dd06b1a662e0c7a88", + "0ec2e8d07e053d686140857493ece653cde52764a60aa1d25651d81811d44a3d", + "4595bb092682af7177799464364c047fbfa7ede767336734949bce5d085fce15", + "6524936709e0719f9e28bf1caac91f86f323d1a79f10ab65a03fa9ea2e6b3270", + "8ba4736edc68da8e5b21c23af4de1f9131f47778c2a5b2a970a799223a9380b3", + "2aaba839e8546a8cb6d04a81ff5be83ac4e700ca4491607a16ebd60dc4995750", + "5f88f7f579ce50d7bcf60d6e9556f77acf361ba54caf3eb61dc4f2ca9c94c6a2", + "d1ca262272613d7386fecce7f1ad268188940b59ea83fdd1f8d7ab85771e3f43", + "5211890e4128d8d730802cc75ee105867e8878581d5e72ffc98a0d60027a5398", + "c1dececcbd7b623bfaa726392512e6ea110db93a0649dd01bf9e559318514f7c", + "43ce93c9835e983403a01329918d184832c5516e1b0ea4e0878b4c4def042777", + "c84724a398bda8d20436fb0663d059c196628fa71192bb720fa5adf6137bed9b", + "f307620a90a36045fa550316d61be4755c1f6c7f3fc81c95fc4d590ac568b2c4", + "78ae0118856a1f17c13f292628fcf3dccc150c7278578caaceb9f35ca8bc9f55", + "9d9a9a40c490f05d7dbafde8bf60d68aab7dcb79a7976eef62d1448a4549816b", + "f6017bec50563dbbb24b979214ae225f96b01dea5b090b174038ff2842a1c6b1", + "6dbec748ea87e959390b0df429c005fccffe9e672864f555c14c7a407b940870", + "11701e76233395ebdba8d1d50334d1a0cc3a158f3452260c9a7b9a067b78d753", + "7354f3829dd05c838aa16d66b00316f7994797927a4d3b594c8af04a2b4ae86a", + "14d254333c6240fbdacbbbe261f7ae15be2e83bffcf084f4f59f459bae0a4571", + "7849ac0f31056265efb0110038f86e61effe17b7db836377d88433b73312e65a", + "9018be885177c675baf9b36978428d841ca30a9185e11de1a31238a08e3ec1af", + "777c5715de7423597fea6c03c496ea46414f0508cd96e119261f002965beb805", + "b742b7509a97c2b346ea762f0510e8475b30dc988417f0f146ae00a800c6dd56", + "7af3029a826e6dfba23ad502901a3632ef4ee047bd45d618b1f106717b0af7ea", + "82390020b2bca461204cc49b20ee6fec1b93dad476726f377785d04040a4d6c3", + "6bc2990699be7f610c9f6a49bc7a19b034040727bc3de63fee89a1573fe00673", + "50c7c2e8e8c7f4aabffb195187a1ac1105f5712859f02d491d794ba2d8498e08", + "5d1d3769cd141603ce907df5dca159e389ca4d75338eddbe1b7db1c0766cbfd6", + "4d8ad7a761d068b7ef9b058fb4ba924b276e7a45a3ad47a79b767e08e821e038", + "d5b5b2167602f7277cdac76cfda8bdad0befcb8ff12df495dfabf40c91d1120c", + "6f2618272bb0164247e9b0f40b19f020c1c9253510dc658a3cc078d079ef01bf", + "fd502d5007bfc62a91eb7a34c1ae23b9de4a20cf5e1bbd86dadc421d7083099c", + "914813583b901044970b9c7d6dcdfde4344339325f5988d0dd681d377f1243fb", + "85cb7cf0245613d08b3027f879f58a6db645bf0db107242beb856b340ee49ee8", + "d6dbb18d5df5e92c8c74ee59aa86d19dff25a5d0f9d030e84285a686f86804d7", + "65309f82cd9f9926a578d09e1ad13909f68a06d02b853760731a4225f7aecef4", + "433519d13f2b64fe82a90cd054797d4779507fea93c1054bd51c35ba846a0f7f", + "62a55d4d6e4b6f992a1c1840f041585c957affcdbf875202cf9e1db9def05aeb", + "e19eb1eeabdeffa04ccd792eb09776690cf2121887b26a7c5d20d66e3e8fd0fb", + "a80951fc593035abb2dda045a2076d29a30f5cc470e0a20db39c3ec588ed61d8", + "4f1b39906714266a7a1936bfa8e08c5d891285dd2b188b4bcd4afb0107671004", + "f46310b2e60727af071d09e759814f52f933688a0ff0995c7da373b381e70005", + "2cc94821f56d21918ba4f2770d97f280e06fd99f4199167e2f4ddc780f404c8a", + "698a2ca7a09e88fa6594801da037f4dbce6ae841d1d33736aebcf8f6fe104e01", ]; diff --git a/src/lib/random.ts b/src/lib/random.ts index 36dde0cc..605471ac 100644 --- a/src/lib/random.ts +++ b/src/lib/random.ts @@ -1,20 +1,20 @@ import { - NumberDictionary, - animals, - colors, - uniqueNamesGenerator, + NumberDictionary, + animals, + colors, + uniqueNamesGenerator, } from "unique-names-generator"; export const randomName = () => - uniqueNamesGenerator({ - dictionaries: [animals, NumberDictionary.generate({ min: 10, max: 99 })], - length: 2, - separator: "", - }); + uniqueNamesGenerator({ + dictionaries: [animals, NumberDictionary.generate({ min: 10, max: 99 })], + length: 2, + separator: "", + }); export const randomPassword = () => - uniqueNamesGenerator({ - dictionaries: [colors, NumberDictionary.generate({ min: 100, max: 999 })], - length: 2, - separator: "", - }); + uniqueNamesGenerator({ + dictionaries: [colors, NumberDictionary.generate({ min: 100, max: 999 })], + length: 2, + separator: "", + }); diff --git a/src/lib/rates.ts b/src/lib/rates.ts index 96aaeac2..5d55550d 100644 --- a/src/lib/rates.ts +++ b/src/lib/rates.ts @@ -4,10 +4,10 @@ let last; let rates; export default async function getRates() { - if (!rates || Date.now() - last > 60000) { - rates = await get("/rates"); - last = Date.now(); - } + if (!rates || Date.now() - last > 60000) { + rates = await get("/rates"); + last = Date.now(); + } - return rates; + return rates; } diff --git a/src/lib/samples.ts b/src/lib/samples.ts index ae7ef6d5..172efca0 100644 --- a/src/lib/samples.ts +++ b/src/lib/samples.ts @@ -1,52 +1,52 @@ import { PUBLIC_COINOS_URL, PUBLIC_DOMAIN } from "$env/static/public"; const api = PUBLIC_DOMAIN.includes("localhost") - ? `${PUBLIC_COINOS_URL}` - : `https://${PUBLIC_DOMAIN}/api`; + ? `${PUBLIC_COINOS_URL}` + : `https://${PUBLIC_DOMAIN}/api`; export default { - me: `curl "${api}/me" -H "content-type: application/json"`, - update: `curl "${api}/user" -H "content-type: application/json" -d '{ + me: `curl "${api}/me" -H "content-type: application/json"`, + update: `curl "${api}/user" -H "content-type: application/json" -d '{ "username": "demo", }'`, - register: `curl "${api}/register" -H "content-type: application/json" -d '{ + register: `curl "${api}/register" -H "content-type: application/json" -d '{ "user": { "username": "demo", "password": "hunter2" } }'`, - login: `curl "${api}/login" -H "content-type: application/json" -d '{ + login: `curl "${api}/login" -H "content-type: application/json" -d '{ "username": "demo", "password": "hunter2" }'`, - payments: `curl "${api}/payments" + payments: `curl "${api}/payments" -H "content-type: application/json" -H "Authorization: Bearer $token" `, - lightning: `curl "${api}/payments" + lightning: `curl "${api}/payments" -H "content-type: application/json" -H "Authorization: Bearer $token" -d '{ "payreq": "lnbc1pj94d8fsp5n77k340ps4m9jn7kp8he8yynpddvurv6mcsrrqpnq5l2jxdxzlwqpp5m96pqhc5nrlk8cqsu9ufdxxa43sarp8vwf9egvm2pg9nl0zu9r8qdq2vdhkjmn0wvxqztgcqpjrzjqwhmav82kntsppmkp8jp4vg4h9nns78tsy8mg7ve4lq5txrkp0h56zlarvqqdtcqqsqqqqqqqqqqp6cq9q9qyysgqzxlypywzphyujm3ga5j5csfcmqvlnae0fgnvymkaaw94eeg7py5rqzysyjkr3ev2snq63qpsc69vf54adkd0szvmvwt5cuadjnuy95sq4xfa40" }'`, - bitcoin: `curl "${api}/bitcoin/send" + bitcoin: `curl "${api}/bitcoin/send" -H "content-type: application/json" -H "Authorization: Bearer $token" -d '{ "amount": 5000, "address": "bc1q3unh97w4rmelflrm2hvdwz37d8kray3vn4d5ca" }'`, - internal: `curl "${api}/send" + internal: `curl "${api}/send" -H "content-type: application/json" -H "Authorization: Bearer $token" -d '{ "amount": 5000, "username": "alice" }'`, - rates: `curl "${api}/rates" + rates: `curl "${api}/rates" -H "content-type: application/json" }'`, - lightningInvoice: `curl "${api}/invoice" + lightningInvoice: `curl "${api}/invoice" -H "content-type: application/json" -H "Authorization: Bearer $token" -d '{ @@ -55,7 +55,7 @@ export default { "type": "lightning" } }'`, - bitcoinAddress: `curl "${api}/invoice" + bitcoinAddress: `curl "${api}/invoice" -H "content-type: application/json" -H "Authorization: Bearer $token" -d '{ @@ -64,7 +64,7 @@ export default { "type": "bitcoin" } }'`, - webhook: `curl "${api}/invoice" + webhook: `curl "${api}/invoice" -H "content-type: application/json" -H "Authorization: Bearer $token" -d '{ @@ -74,9 +74,9 @@ export default { "secret": "webhooksecret" } }'`, - fetchInvoice: `curl "${api}/invoice/bc1qmhfk9stzffhd9umzmld92vff7zg3mdlh7rvvaj" + fetchInvoice: `curl "${api}/invoice/bc1qmhfk9stzffhd9umzmld92vff7zg3mdlh7rvvaj" -H "content-type: application/json"'`, - invoiceResponse: `{ + invoiceResponse: `{ "amount":3141 "tip":0 "type":"bitcoin" @@ -89,5 +89,5 @@ export default { "received":0 "created":1677537428134 }`, - socketAuth: `echo '{"type":"login","data":{"username":"user","password":"password"}}' | websocat -n https://coinos.io/ws`, + socketAuth: `echo '{"type":"login","data":{"username":"user","password":"password"}}' | websocat -n https://coinos.io/ws`, }; diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 6f4d3f62..a3b875c0 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -2,28 +2,28 @@ import { auth, fd, post } from "$lib/utils"; import { fail } from "@sveltejs/kit"; type Error = { - message: string; + message: string; }; export default async ({ cookies, request }) => { - const form = await fd(request); + const form = await fd(request); - if (form.tab === "account") { - form.notify = form.notify === "on"; - form.push = form.push === "on"; - form.nip5 = form.nip5 === "on"; - form.prompt = form.prompt === "on"; - form.autowithdraw = form.autowithdraw === "on"; - } + if (form.tab === "account") { + form.notify = form.notify === "on"; + form.push = form.push === "on"; + form.nip5 = form.nip5 === "on"; + form.prompt = form.prompt === "on"; + form.autowithdraw = form.autowithdraw === "on"; + } - try { - const { user } = await post("/user", form, auth(cookies)); + try { + const { user } = await post("/user", form, auth(cookies)); - if (user.language) cookies.set("lang", user.language, { path: "/" }); + if (user.language) cookies.set("lang", user.language, { path: "/" }); - return { user, success: true }; - } catch (e) { - const { message } = e as Error; - return fail(400, { message }); - } + return { user, success: true }; + } catch (e) { + const { message } = e as Error; + return fail(400, { message }); + } }; diff --git a/src/lib/socket.ts b/src/lib/socket.ts index b87cc165..e40eb39b 100644 --- a/src/lib/socket.ts +++ b/src/lib/socket.ts @@ -12,57 +12,57 @@ let token; export const auth = () => token && send("login", token); export const send = async (type, data) => { - try { - await wait(() => socket?.readyState === 1, 1000, 10); - } catch (e) { - const { message } = e as Error; - if (message === "timeout") reconnectToWebsocket(); - } - - await wait(() => socket?.readyState === 1, 1000, 10); - socket.send(JSON.stringify({ type, data })); + try { + await wait(() => socket?.readyState === 1, 1000, 10); + } catch (e) { + const { message } = e as Error; + if (message === "timeout") reconnectToWebsocket(); + } + + await wait(() => socket?.readyState === 1, 1000, 10); + socket.send(JSON.stringify({ type, data })); }; export const messages = (data) => ({ - id() { - last.set(Date.now()); - }, - invoice() { - invoice.set(data); - }, - event() { - event.set(data); - }, - - request() { - request.set(data); - invalidate("app:invoice"); - }, - - async payment() { - const { amount, confirmed, iid } = data; - invalidate("app:user"); - invalidate("app:invoice"); - invalidate("app:payments"); - - const { - url: { pathname }, - } = get(page); - - const username = cookies.get("username"); - - if (amount > 0) { - if ( - pathname.includes(`/${username}`) || - pathname.includes("/receive") || - pathname.includes("/invoice") - ) { - if (!get(navigating)) goto(`/invoice/${iid}`); - } else success(`${confirmed ? "Received" : "Detected"} ⚡️${s(amount)}!`); - } - }, - - connected: auth, + id() { + last.set(Date.now()); + }, + invoice() { + invoice.set(data); + }, + event() { + event.set(data); + }, + + request() { + request.set(data); + invalidate("app:invoice"); + }, + + async payment() { + const { amount, confirmed, iid } = data; + invalidate("app:user"); + invalidate("app:invoice"); + invalidate("app:payments"); + + const { + url: { pathname }, + } = get(page); + + const username = cookies.get("username"); + + if (amount > 0) { + if ( + pathname.includes(`/${username}`) || + pathname.includes("/receive") || + pathname.includes("/invoice") + ) { + if (!get(navigating)) goto(`/invoice/${iid}`); + } else success(`${confirmed ? "Received" : "Detected"} ⚡️${s(amount)}!`); + } + }, + + connected: auth, }); const initialReconnectDelay = 1000; @@ -72,51 +72,51 @@ let currentReconnectDelay = initialReconnectDelay; let connecting; export function connect(t) { - if (connecting) return; - connecting = true; - setTimeout(() => { - connecting = false; - }, 5000); + if (connecting) return; + connecting = true; + setTimeout(() => { + connecting = false; + }, 5000); - token = t; + token = t; - if (socket) return auth(); + if (socket) return auth(); - socket = new WebSocket(PUBLIC_SOCKET); - socket.addEventListener("open", onWebsocketOpen); - socket.addEventListener("close", onWebsocketClose); - socket.addEventListener("message", onWebsocketMessage); + socket = new WebSocket(PUBLIC_SOCKET); + socket.addEventListener("open", onWebsocketOpen); + socket.addEventListener("close", onWebsocketClose); + socket.addEventListener("message", onWebsocketMessage); } export function close() { - if (socket) socket.close(); + if (socket) socket.close(); } async function onWebsocketMessage(msg) { - const { type, data } = JSON.parse(msg.data); - if (get(navigating)) await sleep(2000); + const { type, data } = JSON.parse(msg.data); + if (get(navigating)) await sleep(2000); - if (messages(data)[type]) messages(data)[type](); + if (messages(data)[type]) messages(data)[type](); } function onWebsocketOpen() { - currentReconnectDelay = initialReconnectDelay; - send("heartbeat", token); + currentReconnectDelay = initialReconnectDelay; + send("heartbeat", token); } function onWebsocketClose() { - socket = null; - setTimeout( - () => { - reconnectToWebsocket(); - }, - currentReconnectDelay + Math.floor(Math.random() * 3000), - ); + socket = null; + setTimeout( + () => { + reconnectToWebsocket(); + }, + currentReconnectDelay + Math.floor(Math.random() * 3000), + ); } function reconnectToWebsocket() { - if (currentReconnectDelay < maxReconnectDelay) { - currentReconnectDelay *= 2; - } - connect(token); + if (currentReconnectDelay < maxReconnectDelay) { + currentReconnectDelay *= 2; + } + connect(token); } diff --git a/src/lib/store.ts b/src/lib/store.ts index 7cd17aec..e14d107f 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -2,73 +2,73 @@ import { browser } from "$app/environment"; import { writable } from "svelte/store"; export const persistSession = (key, defaultValue = undefined) => { - const s = writable( - browser && - sessionStorage.getItem(key) && - sessionStorage.getItem(key) !== "undefined" - ? JSON.parse(sessionStorage.getItem(key) || "") - : defaultValue, - ); + const s = writable( + browser && + sessionStorage.getItem(key) && + sessionStorage.getItem(key) !== "undefined" + ? JSON.parse(sessionStorage.getItem(key) || "") + : defaultValue, + ); - s.subscribe((v) => { - browser && sessionStorage.setItem(key, JSON.stringify(v)); - }); + s.subscribe((v) => { + browser && sessionStorage.setItem(key, JSON.stringify(v)); + }); - return s; + return s; }; export const persistLocal = (key, defaultValue: any = undefined) => { - const s = writable( - browser && - localStorage.getItem(key) && - localStorage.getItem(key) !== "undefined" - ? JSON.parse(localStorage.getItem(key) || "") - : defaultValue, - ); + const s = writable( + browser && + localStorage.getItem(key) && + localStorage.getItem(key) !== "undefined" + ? JSON.parse(localStorage.getItem(key) || "") + : defaultValue, + ); - s.subscribe((v) => { - try { - if (browser) { - if (v) { - localStorage.setItem(key, JSON.stringify(v)); - } else { - localStorage.removeItem(key); - } - } - } catch (e) { - console.log("problem setting key", v); - console.log(e); - } - }); + s.subscribe((v) => { + try { + if (browser) { + if (v) { + localStorage.setItem(key, JSON.stringify(v)); + } else { + localStorage.removeItem(key); + } + } + } catch (e) { + console.log("problem setting key", v); + console.log(e); + } + }); - return s; + return s; }; export const persistLocalValue = (key, defaultValue: any = undefined) => { - const s = writable( - browser && - localStorage.getItem(key) && - localStorage.getItem(key) !== "undefined" - ? JSON.parse(localStorage.getItem(key) || "") - : defaultValue, - ); + const s = writable( + browser && + localStorage.getItem(key) && + localStorage.getItem(key) !== "undefined" + ? JSON.parse(localStorage.getItem(key) || "") + : defaultValue, + ); - s.subscribe((v) => { - try { - if (browser) { - if (v === undefined) { - localStorage.removeItem(key); - } else { - localStorage.setItem(key, JSON.stringify(v)); - } - } - } catch (e) { - console.log("problem setting key", v); - console.log(e); - } - }); + s.subscribe((v) => { + try { + if (browser) { + if (v === undefined) { + localStorage.removeItem(key); + } else { + localStorage.setItem(key, JSON.stringify(v)); + } + } + } catch (e) { + console.log("problem setting key", v); + console.log(e); + } + }); - return s; + return s; }; export const account = writable(); diff --git a/src/lib/ticket.ts b/src/lib/ticket.ts index 0d93f390..48ed1a66 100644 --- a/src/lib/ticket.ts +++ b/src/lib/ticket.ts @@ -3,29 +3,29 @@ import { env } from "$env/dynamic/private"; import tickets from "$lib/tickets"; export const createTicket = async (cookies, username) => { - const { ticket } = await get("/ticket"); + const { ticket } = await get("/ticket"); - const { address } = await post( - `/${username}/invoice`, - { invoice: { network: "liquid" }, user: { username } }, - auth(cookies), - ); + const { address } = await post( + `/${username}/invoice`, + { invoice: { network: "liquid" }, user: { username } }, + auth(cookies), + ); - const { asset } = await post( - "/assets", - { - address, - name: `Launch Party Ticket ${ticket}`, - ticker: "LPT", - precision: 0, - asset_amount: "1", - token_amount: 0, - domain: "coinos.io", - filename: tickets[ticket], - }, - { authorization: `Bearer ${env.LAUNCH}` }, - ); + const { asset } = await post( + "/assets", + { + address, + name: `Launch Party Ticket ${ticket}`, + ticker: "LPT", + precision: 0, + asset_amount: "1", + token_amount: 0, + domain: "coinos.io", + filename: tickets[ticket], + }, + { authorization: `Bearer ${env.LAUNCH}` }, + ); - await post("/ticket", { asset }, { authorization: `Bearer ${env.LAUNCH}` }); - return asset; + await post("/ticket", { asset }, { authorization: `Bearer ${env.LAUNCH}` }); + return asset; }; diff --git a/src/lib/tickets.ts b/src/lib/tickets.ts index f37fad78..1f8cbdac 100644 --- a/src/lib/tickets.ts +++ b/src/lib/tickets.ts @@ -1,52 +1,52 @@ export default [ - "QmPbFkiXNuaQyfwD4W9GcXP7qWaNd6wFd9KCnJir5mes1J", - "QmPVjvbaMxSJRpDxMLYTdUWQbknej1vYLckBQ2pAgxZgJN", - "QmVasycSFzCYhyXrcvTE7mN3d6W78RFnrhzg5cRF6Dp6Zq", - "QmTmnhjpTBjZdeHwXgT9Dftc3uWMW73qsZpZ5ECrTyiKKZ", - "QmWDBiyVPyFkzUrEdvy7DSJe1rBQpene1m6quKAtyjBP6a", - "QmcxxzQZwgG7SE9QbHNvoTiCi6Dhs9rrz9roqvxm2JQA88", - "Qmd9ThEpXLLHimEzRUWEFrAGLSw5PAHzn2SmwfCnjNLwWt", - "Qmc7pWw1yxc2ARBWitDMYiyPkWK61NsXXU8oYAQwNAvcx1", - "QmX9Hgw1rMFUgBBuq8QfZZDzLjN5xCED19fwFdDeDDqwmr", - "QmconCAyNyaagqKvvfKCbG7Eu8c6vCtkv2ivvxMoFN9YwA", - "QmWbFmvViy5gx4JdD5sw5YJETet4ZQcXtYgijYnrsnSZ3a", - "QmP6QiiJwoqkduZMqdd5e5htHZ4aYHCsvEu7WEEUknKYa5", - "QmefHk8hvBUHsuKugntok5u75Q8i886cnPwsj4LZZB6uWg", - "QmdrSDK5ByYHxc88hfDu8hQgK5XYFpXSfPorUXdyTbE437", - "QmZ6ZycjRzYpsjnLm1Lfgk1fXXDdhcYRkyfmqYJZVGwADy", - "QmSkBxS5yLPjB2VPs89oQRZANsoyodK6s8s52E8GKXr2zt", - "QmSn8TCPwq3UZ1Z4eyko76CvQo2H2Hyef1NGboDQN22m5i", - "QmP6CTH5zrKMzZwtjy7VaK9NXiq4Gw53kmuRjUbJjeovoe", - "QmdwT4yvz9Xf8g3t6irKRoh3Wa7MwRQrcUJckQxyQe1Bc5", - "QmcwuHH3bD5JExqjDvKqRQ3V5xEVGqs94qPNVDzaCqJa5a", - "QmRfVduHwn8sXwSoyfgZpxSi8L7EjP4LMp6fmijo9VApDp", - "Qmc4TGj1Ahbg9HJ9WDXDm2bXsNkT4zRDp8Wdw9wqqdEwuH", - "QmY8uQiHMWA8ztf2wAurNFW7eiTHQEUiLTQ2vGceMwVqz3", - "QmXiXbaD12F4mkbUSQcKhWshkPU4oi29cUawZGnPk86VVU", - "QmZaiAGjvBZBw4suz33s9JQNzY2Kshean2mQQPCa6gKMQe", - "Qme8bDSdzX7tanPYf672xXW8vPdkLkeSLqLzHJqidRXsYJ", - "QmRJirjuUKgh2AMyBF4jNquaXHFPBtDJf3gKFC614hX7qu", - "QmdRoSKSgabcrpekNBPB6T47RBtJnCETYE8ry2a3P37MUe", - "QmXoqArruHSghrUGW35vVQHPknBG5Cj939iWWHHXU3B616", - "QmYCxtDuoHLMAFhNpifUsLU9ptAeFKwyxSiSEJhDQLMZtN", - "QmQZQBw8wgzUsrmVDEejdePK23AEJbk9aUracvxLPyHz2x", - "QmSsbu7U3i62AQKuYqfisRo5E6nqdMZfHRKWYJgouaXDxD", - "QmaP4adVQuWyqtN4QCXeYXj4nr19Je4EqsaTG1zM9ywqFS", - "QmPPsvgfXsiN3wNqdAwHAhQVpw6SZWNpry9Na6R2Xb7zXY", - "QmR7z6wQmcfZUvRsVK83GRRboyvwZEDQujLwumMY33nAit", - "QmQXgkgorDLPUyQpFbW8aLUW6jioQjUKamtXw5UuNCttDg", - "QmcwYBeXt1BPfdSrtSrXFpWctRZ7RDHvmKTttC5hPPTSzG", - "QmTUexZnw3xPy3YVdUsqJALwTvy2qFax5b3QpJVMvTCJQb", - "QmNLkYXFUvctpfeeDTZxruZE2f834EUnYMM6JJUeTcUB4J", - "QmT7544UaTPgKFYMCpPE4unXphNGX71BLZBPKLkxAijSmj", - "QmTpVR4dqX1QV4fae5pZ1ACtRcANHwEWCLwpMXFmwE95Sm", - "Qmdfyps5fW8d8wstXMfpvpei2DTmaADBWgmxLT7e7uan4P", - "QmUSK8byipYoG3H97QpmPrHw9vYrvHygyhkiM81XmaKyxS", - "QmQUGRvp96LTw5i8Ly9f5LoMvUpeQHCLGNqC88KV74wEHK", - "Qma693TFLh9Z1MeBFzhPHi6Yk4tmTcZY2jogwJ8cPkU2B4", - "QmNu8FyMpmkFdbCXGRVG8mKJtMvvCoLP8LNzFsxBQwxQeS", - "QmWZHE87PKujL1MGLuFgP3D12o5GQxYbvbeTRgr12f1XMu", - "QmcZJn8gPGd3poS9yuiNJh6iP4cn9uy5fnypky71r4sSSw", - "QmTwrAF4E9ucpdnrUZFj6EfrQamGv7Qyn7ceSx1DUQYceC", - "QmexssxU5iTXypgNqrmrQwMDJnTNxyZUxor5wDLnwb5TZp", + "QmPbFkiXNuaQyfwD4W9GcXP7qWaNd6wFd9KCnJir5mes1J", + "QmPVjvbaMxSJRpDxMLYTdUWQbknej1vYLckBQ2pAgxZgJN", + "QmVasycSFzCYhyXrcvTE7mN3d6W78RFnrhzg5cRF6Dp6Zq", + "QmTmnhjpTBjZdeHwXgT9Dftc3uWMW73qsZpZ5ECrTyiKKZ", + "QmWDBiyVPyFkzUrEdvy7DSJe1rBQpene1m6quKAtyjBP6a", + "QmcxxzQZwgG7SE9QbHNvoTiCi6Dhs9rrz9roqvxm2JQA88", + "Qmd9ThEpXLLHimEzRUWEFrAGLSw5PAHzn2SmwfCnjNLwWt", + "Qmc7pWw1yxc2ARBWitDMYiyPkWK61NsXXU8oYAQwNAvcx1", + "QmX9Hgw1rMFUgBBuq8QfZZDzLjN5xCED19fwFdDeDDqwmr", + "QmconCAyNyaagqKvvfKCbG7Eu8c6vCtkv2ivvxMoFN9YwA", + "QmWbFmvViy5gx4JdD5sw5YJETet4ZQcXtYgijYnrsnSZ3a", + "QmP6QiiJwoqkduZMqdd5e5htHZ4aYHCsvEu7WEEUknKYa5", + "QmefHk8hvBUHsuKugntok5u75Q8i886cnPwsj4LZZB6uWg", + "QmdrSDK5ByYHxc88hfDu8hQgK5XYFpXSfPorUXdyTbE437", + "QmZ6ZycjRzYpsjnLm1Lfgk1fXXDdhcYRkyfmqYJZVGwADy", + "QmSkBxS5yLPjB2VPs89oQRZANsoyodK6s8s52E8GKXr2zt", + "QmSn8TCPwq3UZ1Z4eyko76CvQo2H2Hyef1NGboDQN22m5i", + "QmP6CTH5zrKMzZwtjy7VaK9NXiq4Gw53kmuRjUbJjeovoe", + "QmdwT4yvz9Xf8g3t6irKRoh3Wa7MwRQrcUJckQxyQe1Bc5", + "QmcwuHH3bD5JExqjDvKqRQ3V5xEVGqs94qPNVDzaCqJa5a", + "QmRfVduHwn8sXwSoyfgZpxSi8L7EjP4LMp6fmijo9VApDp", + "Qmc4TGj1Ahbg9HJ9WDXDm2bXsNkT4zRDp8Wdw9wqqdEwuH", + "QmY8uQiHMWA8ztf2wAurNFW7eiTHQEUiLTQ2vGceMwVqz3", + "QmXiXbaD12F4mkbUSQcKhWshkPU4oi29cUawZGnPk86VVU", + "QmZaiAGjvBZBw4suz33s9JQNzY2Kshean2mQQPCa6gKMQe", + "Qme8bDSdzX7tanPYf672xXW8vPdkLkeSLqLzHJqidRXsYJ", + "QmRJirjuUKgh2AMyBF4jNquaXHFPBtDJf3gKFC614hX7qu", + "QmdRoSKSgabcrpekNBPB6T47RBtJnCETYE8ry2a3P37MUe", + "QmXoqArruHSghrUGW35vVQHPknBG5Cj939iWWHHXU3B616", + "QmYCxtDuoHLMAFhNpifUsLU9ptAeFKwyxSiSEJhDQLMZtN", + "QmQZQBw8wgzUsrmVDEejdePK23AEJbk9aUracvxLPyHz2x", + "QmSsbu7U3i62AQKuYqfisRo5E6nqdMZfHRKWYJgouaXDxD", + "QmaP4adVQuWyqtN4QCXeYXj4nr19Je4EqsaTG1zM9ywqFS", + "QmPPsvgfXsiN3wNqdAwHAhQVpw6SZWNpry9Na6R2Xb7zXY", + "QmR7z6wQmcfZUvRsVK83GRRboyvwZEDQujLwumMY33nAit", + "QmQXgkgorDLPUyQpFbW8aLUW6jioQjUKamtXw5UuNCttDg", + "QmcwYBeXt1BPfdSrtSrXFpWctRZ7RDHvmKTttC5hPPTSzG", + "QmTUexZnw3xPy3YVdUsqJALwTvy2qFax5b3QpJVMvTCJQb", + "QmNLkYXFUvctpfeeDTZxruZE2f834EUnYMM6JJUeTcUB4J", + "QmT7544UaTPgKFYMCpPE4unXphNGX71BLZBPKLkxAijSmj", + "QmTpVR4dqX1QV4fae5pZ1ACtRcANHwEWCLwpMXFmwE95Sm", + "Qmdfyps5fW8d8wstXMfpvpei2DTmaADBWgmxLT7e7uan4P", + "QmUSK8byipYoG3H97QpmPrHw9vYrvHygyhkiM81XmaKyxS", + "QmQUGRvp96LTw5i8Ly9f5LoMvUpeQHCLGNqC88KV74wEHK", + "Qma693TFLh9Z1MeBFzhPHi6Yk4tmTcZY2jogwJ8cPkU2B4", + "QmNu8FyMpmkFdbCXGRVG8mKJtMvvCoLP8LNzFsxBQwxQeS", + "QmWZHE87PKujL1MGLuFgP3D12o5GQxYbvbeTRgr12f1XMu", + "QmcZJn8gPGd3poS9yuiNJh6iP4cn9uy5fnypky71r4sSSw", + "QmTwrAF4E9ucpdnrUZFj6EfrQamGv7Qyn7ceSx1DUQYceC", + "QmexssxU5iTXypgNqrmrQwMDJnTNxyZUxor5wDLnwb5TZp", ]; diff --git a/src/lib/types.ts b/src/lib/types.ts index 19277ec1..827f96aa 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,23 +1,23 @@ export interface Message { - author: User; - created_at: number; - recipient: User; + author: User; + created_at: number; + recipient: User; } export interface Note { - created_at: number; - seen: number; + created_at: number; + seen: number; } export interface User { - id: string; + id: string; } export interface Invoice { - aid: string; - id?: string; - memoPrompt?: boolean; - memo?: string; - rate: number; - type?: string; + aid: string; + id?: string; + memoPrompt?: boolean; + memo?: string; + rate: number; + type?: string; } diff --git a/src/lib/upload.ts b/src/lib/upload.ts index 6d9d4230..aaeec007 100644 --- a/src/lib/upload.ts +++ b/src/lib/upload.ts @@ -1,18 +1,18 @@ export const upload = async (file, type, progress) => { - const url = `/api/upload/${type}`; - const formData = new FormData(); - formData.append("file", file); + const url = `/api/upload/${type}`; + const formData = new FormData(); + formData.append("file", file); - return new Promise((resolve, reject) => { - const ajax = new XMLHttpRequest(); - ajax.addEventListener("load", () => { - if (ajax.readyState === ajax.DONE) { - if (ajax.status === 200) resolve(ajax.response); - else reject(new Error(`Upload failed: ${ajax.response}`)); - } - }); - ajax.upload.addEventListener("progress", progress, false); - ajax.open("POST", url); - ajax.send(formData); - }); + return new Promise((resolve, reject) => { + const ajax = new XMLHttpRequest(); + ajax.addEventListener("load", () => { + if (ajax.readyState === ajax.DONE) { + if (ajax.status === 200) resolve(ajax.response); + else reject(new Error(`Upload failed: ${ajax.response}`)); + } + }); + ajax.upload.addEventListener("progress", progress, false); + ajax.open("POST", url); + ajax.send(formData); + }); }; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 45afb90b..ebec6caa 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -6,16 +6,16 @@ import { bytesToHex } from "@noble/hashes/utils.js"; import { fail as svelteFail, redirect } from "@sveltejs/kit"; import { toast } from "@zerodevx/svelte-toast"; import { - differenceInDays, - differenceInHours, - differenceInMinutes, - differenceInSeconds, + differenceInDays, + differenceInHours, + differenceInMinutes, + differenceInSeconds, } from "date-fns"; import { get as getStore } from "svelte/store"; export function scroll(section: any) { - if (getStore(page).url.pathname !== "/") goto("/"); - setTimeout(() => section.scrollIntoView({ behavior: "smooth" }), 500); + if (getStore(page).url.pathname !== "/") goto("/"); + setTimeout(() => section.scrollIntoView({ behavior: "smooth" }), 500); } const base = browser ? "" : PUBLIC_COINOS_URL; @@ -23,523 +23,530 @@ const base = browser ? "" : PUBLIC_COINOS_URL; let getIp: (() => string | undefined) | undefined; export const setIpGetter = (fn: () => string | undefined) => { - getIp = fn; + getIp = fn; }; const ipHeaders = (): Record => { - const ip = getIp?.(); - return ip ? { "cf-connecting-ip": ip } : {}; + const ip = getIp?.(); + return ip ? { "cf-connecting-ip": ip } : {}; }; export const punk = (k: string) => - `${Math.floor((parseInt(k.slice(-2), 16) / 256) * 64) + 1}.webp`; + `${Math.floor((parseInt(k.slice(-2), 16) / 256) * 64) + 1}.webp`; export const g = (url: string, fetch: any, headers: object) => - fetch(base + url, { headers: { ...ipHeaders(), ...headers } }) - .then((r: Response) => r.text()) - .then((body: string) => { - try { - return JSON.parse(body); - } catch (e) { - throw new Error(body); - } - }); + fetch(base + url, { headers: { ...ipHeaders(), ...headers } }) + .then((r: Response) => r.text()) + .then((body: string) => { + try { + return JSON.parse(body); + } catch { + throw new Error(body); + } + }); export const get = (url: string, headers = {}) => { - return fetch(base + url, { headers: { ...ipHeaders(), ...headers } }) - .then(async (r) => { - if (r.ok) return r.text(); - throw new Error(await r.text()); - }) - .then((body) => { - try { - return JSON.parse(body); - } catch (e) { - return body; - } - }); + return fetch(base + url, { headers: { ...ipHeaders(), ...headers } }) + .then(async (r) => { + if (r.ok) return r.text(); + throw new Error(await r.text()); + }) + .then((body) => { + try { + return JSON.parse(body); + } catch { + return body; + } + }); }; export const post = async ( - url: string, - body: object, - headers: HeadersInit | undefined = undefined, + url: string, + body: object, + headers: HeadersInit | undefined = undefined, ) => { - headers = { - "content-type": "application/json", - accept: "application/json", - ...ipHeaders(), - ...headers, - }; - - const response = await fetch(base + url, { - method: "POST", - body: JSON.stringify(body), - headers, - }) - .then(async (r) => { - if (r.ok) return r.text(); - const text = await r.text(); - - let message; - try { - ({ message } = JSON.parse(text)); - } catch (e) {} - - if (message) throw new Error(message); - throw new Error(text); - }) - .then((body) => { - let json; - try { - json = JSON.parse(body); - } catch (e) { - throw new Error(body); - } - - if (json.error instanceof Error) throw json.error; - if (json instanceof Error) throw body; - if (json.name === "Error") throw new Error(json.message); - - return json; - }); - - return response; + headers = { + "content-type": "application/json", + accept: "application/json", + ...ipHeaders(), + ...headers, + }; + + const response = await fetch(base + url, { + method: "POST", + body: JSON.stringify(body), + headers, + }) + .then(async (r) => { + if (r.ok) return r.text(); + const text = await r.text(); + + let message; + try { + ({ message } = JSON.parse(text)); + } catch {} + + if (message) throw new Error(message); + throw new Error(text); + }) + .then((body) => { + let json; + try { + json = JSON.parse(body); + } catch { + throw new Error(body); + } + + if (json.error instanceof Error) throw json.error; + if (json instanceof Error) throw body; + if (json.name === "Error") throw new Error(json.message); + + return json; + }); + + return response; }; export const copy = (text: string) => { - navigator.clipboard.writeText(text); - success("Copied!"); + navigator.clipboard.writeText(text); + success("Copied!"); }; export const copyNoNewlines = (text: string) => { - const stripped = text.replace(/\n/g, " ").replace(/\s+/g, " "); - navigator.clipboard.writeText(stripped); - success("Copied!"); + const stripped = text.replace(/\n/g, " ").replace(/\s+/g, " "); + navigator.clipboard.writeText(stripped); + success("Copied!"); }; let recent: string[] = []; export const success = (m: string, clear = true) => { - if (recent.includes(m)) return; - recent.push(m); - setTimeout(() => (recent = []), 5000); - if (clear) toast.pop(); - toast.push(m, { - theme: { - "--toastBarBackground": "#F5F7FA", - }, - }); + if (recent.includes(m)) return; + recent.push(m); + setTimeout(() => (recent = []), 5000); + if (clear) toast.pop(); + toast.push(m, { + theme: { + "--toastBarBackground": "#F5F7FA", + }, + }); }; export const warning = (m: string, clear = true) => { - if (clear) toast.pop(); - toast.push(m, { - theme: { - "--toastBarBackground": "#FFCE22", - }, - }); + if (clear) toast.pop(); + toast.push(m, { + theme: { + "--toastBarBackground": "#FFCE22", + }, + }); }; export const fail = (m: string, clear = true) => { - if (clear) toast.pop(); - toast.push(m, { - theme: { - "--toastBarBackground": "#E93535", - }, - }); + if (clear) toast.pop(); + toast.push(m, { + theme: { + "--toastBarBackground": "#E93535", + }, + }); }; export const info = (m: string) => { - toast.pop(); - toast.push(m, { - theme: { - "--toastBarBackground": "white", - }, - }); + toast.pop(); + toast.push(m, { + theme: { + "--toastBarBackground": "white", + }, + }); }; export const login = async ( - user: { - username: string; - password: string; - token?: string; - challenge?: string; - recaptcha?: string; - }, - cookies, - ip: string, - host?: string, - extraHeaders?: Record, + user: { + username: string; + password: string; + token?: string; + challenge?: string; + recaptcha?: string; + }, + cookies, + ip: string, + host?: string, + extraHeaders?: Record, ) => { - const maxAge = 380 * 24 * 60 * 60; - - const headers: Record = { - "content-type": "application/json", - accept: "application/json", - "cf-connecting-ip": ip, - ...extraHeaders, - }; - if (host) headers["x-forwarded-host"] = host; - - const res = await fetch(`${base}/login`, { - method: "POST", - body: JSON.stringify(user), - headers, - }); - - const text = await res.text(); - - if (res.status === 401) { - if (text.startsWith("2fa")) throw new Error("2fa"); - throw new Error(text); - } - - const { user: u, token } = JSON.parse(text); - if (!token) throw new Error("Login failed"); - - const expires = new Date(); - expires.setSeconds(expires.getSeconds() + maxAge); - - const opts = { path: "/", expires }; - if (u.language) cookies.set("lang", u.language, opts); - cookies.set("username", user.username, opts); - cookies.set("token", token, opts); - if (u.nsec) { - try { - const { decrypt } = await import("nostr-tools/nip49"); - cookies.set("sk", bytesToHex(decrypt(u.nsec, user.password)), { - ...opts, - httpOnly: false, - sameSite: "lax", - }); - } catch (e) {} - } + const maxAge = 380 * 24 * 60 * 60; + + const headers: Record = { + "content-type": "application/json", + accept: "application/json", + "cf-connecting-ip": ip, + ...extraHeaders, + }; + if (host) headers["x-forwarded-host"] = host; + + const res = await fetch(`${base}/login`, { + method: "POST", + body: JSON.stringify(user), + headers, + }); + + const text = await res.text(); + + if (res.status === 401) { + if (text.startsWith("2fa")) throw new Error("2fa"); + throw new Error(text); + } + + const { user: u, token } = JSON.parse(text); + if (!token) throw new Error("Login failed"); + + const expires = new Date(); + expires.setSeconds(expires.getSeconds() + maxAge); + + const opts = { path: "/", expires }; + if (u.language) cookies.set("lang", u.language, opts); + cookies.set("username", user.username, opts); + cookies.set("token", token, opts); + if (u.nsec) { + try { + const { decrypt } = await import("nostr-tools/nip49"); + cookies.set("sk", bytesToHex(decrypt(u.nsec, user.password)), { + ...opts, + httpOnly: false, + sameSite: "lax", + }); + } catch {} + } }; export const auth = (cookies) => ({ - authorization: `Bearer ${cookies.get("token")}`, + authorization: `Bearer ${cookies.get("token")}`, }); export const isInvalidTokenError = (e: unknown) => { - const message = (e as Error)?.message || ""; - if (!message) return false; - const normalized = message.toLowerCase(); - return ( - normalized.includes("jwt") || - normalized.includes("invalid token") || - normalized.includes("token expired") || - normalized.includes("expired token") || - normalized.includes("unauthorized") || - normalized.includes("not authorized") - ); + const message = (e as Error)?.message || ""; + if (!message) return false; + const normalized = message.toLowerCase(); + return ( + normalized.includes("jwt") || + normalized.includes("invalid token") || + normalized.includes("token expired") || + normalized.includes("expired token") || + normalized.includes("unauthorized") || + normalized.includes("not authorized") + ); }; export const btc = (fiat: number, rate: number): number => - Math.round((fiat * sats) / rate); + Math.round((fiat * sats) / rate); export const toFiat = (amount: number, rate: number): number => - (amount * rate) / sats; + (amount * rate) / sats; export const fd = async (req: Request): Promise => { - const obj: Record = Object.fromEntries(await req.formData()); - for (const k in obj) - (obj[k] === "undefined" && (obj[k] = undefined)) || - (obj[k] === "false" && (obj[k] = false)); - return obj; + const obj: Record = Object.fromEntries(await req.formData()); + for (const k in obj) + (obj[k] === "undefined" && (obj[k] = undefined)) || + (obj[k] === "false" && (obj[k] = false)); + return obj; }; const map = { - en: { - CAD: "en-CA", - USD: "en-US", - GBP: "en-GB", - AUD: "en-AU", - EUR: "en-IE", // Eurozone English (Ireland) - NZD: "en-NZ", - JPY: "en-JP", // English in Japan - INR: "en-IN", // English in India - ZAR: "en-ZA", // South Africa - }, - fr: { - CAD: "fr-CA", // French Canada - EUR: "fr-FR", // France - CHF: "fr-CH", // French Switzerland - XOF: "fr-SN", // Francophone West Africa - }, - de: { - EUR: "de-DE", // Germany - CHF: "de-CH", // German Switzerland - USD: "de-US", // German for the US - }, - es: { - EUR: "es-ES", // Spain - USD: "es-US", // Spanish for the US - MXN: "es-MX", // Mexico - ARS: "es-AR", // Argentina - CLP: "es-CL", // Chile - }, - pt: { - EUR: "pt-PT", // Portugal - BRL: "pt-BR", // Brazil - }, - zh: { - CNY: "zh-CN", // Simplified Chinese (Mainland China) - HKD: "zh-HK", // Traditional Chinese (Hong Kong) - TWD: "zh-TW", // Traditional Chinese (Taiwan) - USD: "zh-US", // Chinese for the US - }, - ja: { - JPY: "ja-JP", // Japan - USD: "ja-US", // Japanese for the US - }, - ru: { - RUB: "ru-RU", // Russia - USD: "ru-US", // Russian for the US - }, - el: { - EUR: "el-GR", // Greece - }, - fa: { - IRR: "fa-IR", // Iran - }, - // Global currency mappings (fallbacks) - CAD: "en-CA", - USD: "en-US", - GBP: "en-GB", - AUD: "en-AU", - EUR: "en-IE", - NZD: "en-NZ", - JPY: "ja-JP", - INR: "en-IN", - ZAR: "en-ZA", - CHF: "de-CH", - MXN: "es-MX", - BRL: "pt-BR", - CNY: "zh-CN", - HKD: "zh-HK", - TWD: "zh-TW", - RUB: "ru-RU", - IRR: "fa-IR", + en: { + CAD: "en-CA", + USD: "en-US", + GBP: "en-GB", + AUD: "en-AU", + EUR: "en-IE", // Eurozone English (Ireland) + NZD: "en-NZ", + JPY: "en-JP", // English in Japan + INR: "en-IN", // English in India + ZAR: "en-ZA", // South Africa + }, + fr: { + CAD: "fr-CA", // French Canada + EUR: "fr-FR", // France + CHF: "fr-CH", // French Switzerland + XOF: "fr-SN", // Francophone West Africa + }, + de: { + EUR: "de-DE", // Germany + CHF: "de-CH", // German Switzerland + USD: "de-US", // German for the US + }, + es: { + EUR: "es-ES", // Spain + USD: "es-US", // Spanish for the US + MXN: "es-MX", // Mexico + ARS: "es-AR", // Argentina + CLP: "es-CL", // Chile + }, + pt: { + EUR: "pt-PT", // Portugal + BRL: "pt-BR", // Brazil + }, + zh: { + CNY: "zh-CN", // Simplified Chinese (Mainland China) + HKD: "zh-HK", // Traditional Chinese (Hong Kong) + TWD: "zh-TW", // Traditional Chinese (Taiwan) + USD: "zh-US", // Chinese for the US + }, + ja: { + JPY: "ja-JP", // Japan + USD: "ja-US", // Japanese for the US + }, + ru: { + RUB: "ru-RU", // Russia + USD: "ru-US", // Russian for the US + }, + el: { + EUR: "el-GR", // Greece + }, + fa: { + IRR: "fa-IR", // Iran + }, + // Global currency mappings (fallbacks) + CAD: "en-CA", + USD: "en-US", + GBP: "en-GB", + AUD: "en-AU", + EUR: "en-IE", + NZD: "en-NZ", + JPY: "ja-JP", + INR: "en-IN", + ZAR: "en-ZA", + CHF: "de-CH", + MXN: "es-MX", + BRL: "pt-BR", + CNY: "zh-CN", + HKD: "zh-HK", + TWD: "zh-TW", + RUB: "ru-RU", + IRR: "fa-IR", }; export const loc = (user) => { - if (!user) return; - const { currency: c, language: l } = user; - return map[l]?.[c] || map[c]; + if (!user) return; + const { currency: c, language: l } = user; + return map[l]?.[c] || map[c]; }; export const f = ( - s: number, - currency: string, - locale = "en-US", - minimumFractionDigits = 2, - maximumFractionDigits = 2, + s: number, + currency: string, + locale = "en-US", + minimumFractionDigits = 2, + maximumFractionDigits = 2, ) => { - const l = typeof locale === "function" ? locale() : locale || "en-US"; - const cur = currency || "USD"; - const region = l.length >= 2 ? l.slice(-2) : ""; - const currencyDisplay = - region && cur.startsWith(region) ? "narrowSymbol" : "symbol"; - return new Intl.NumberFormat(l, { - style: "currency", - currency: cur, - currencyDisplay, - minimumFractionDigits, - maximumFractionDigits, - }).format(s); + const l = typeof locale === "function" ? locale() : locale || "en-US"; + const cur = currency || "USD"; + const region = l.length >= 2 ? l.slice(-2) : ""; + const currencyDisplay = + region && cur.startsWith(region) ? "narrowSymbol" : "symbol"; + return new Intl.NumberFormat(l, { + style: "currency", + currency: cur, + currencyDisplay, + minimumFractionDigits, + maximumFractionDigits, + }).format(s); }; export const s = (s: number, locale = "en-US"): string => { - const l = typeof locale === "function" ? locale() : locale || "en-US"; - return new Intl.NumberFormat(l, { maximumFractionDigits: 0 }).format(s); + const l = typeof locale === "function" ? locale() : locale || "en-US"; + return new Intl.NumberFormat(l, { maximumFractionDigits: 0 }).format(s); }; export const sat = (s: number): string => `${si(Math.abs(s)).toString()}`; export const si = ( - n: number, - minimumFractionDigits = 1, - maximumFractionDigits = 2, + n: number, + minimumFractionDigits = 1, + maximumFractionDigits = 2, ) => { - const units = ["", "K", "M", "G", "T", "P", "E", "Z"]; - - let s = n; - let d = 0; - if (s >= 1000) { - d = Math.floor(Math.log10(s) / 3); // Calculate the unit index - s = n / 1000 ** d; // Scale the number to the correct unit - } - - return ( - new Intl.NumberFormat("en-US", { - minimumFractionDigits, - maximumFractionDigits, - }).format(s) + units[d] - ); + const units = ["", "K", "M", "G", "T", "P", "E", "Z"]; + + let s = n; + let d = 0; + if (s >= 1000) { + d = Math.floor(Math.log10(s) / 3); // Calculate the unit index + s = n / 1000 ** d; // Scale the number to the correct unit + } + + return ( + new Intl.NumberFormat("en-US", { + minimumFractionDigits, + maximumFractionDigits, + }).format(s) + units[d] + ); }; export const sats = 100000000; export const back = (): any => - browser && (history.length ? history.go(-1) : goto("/")); + browser && (history.length ? history.go(-1) : goto("/")); export const focus = (el: HTMLElement): any => - browser && screen.width > 1280 && setTimeout(() => el.focus(), 1); + browser && screen.width > 1280 && setTimeout(() => el.focus(), 1); export const select = (el: HTMLInputElement): any => - browser && screen.width > 1280 && setTimeout(() => el.select(), 1); + browser && screen.width > 1280 && setTimeout(() => el.select(), 1); export const sleep = (n: number): Promise => - new Promise((r) => setTimeout(r, n)); + new Promise((r) => setTimeout(r, n)); export const wait = async ( - f: () => boolean | Promise, - n = 100, - s = 300, + f: () => boolean | Promise, + n = 100, + s = 300, ): Promise => { - let i = 0; - while (!(await f()) && i < s) { - await sleep(n); - i++; - } - if (i >= s) throw new Error("timeout"); - return f(); + let i = 0; + while (!(await f()) && i < s) { + await sleep(n); + i++; + } + if (i >= s) throw new Error("timeout"); + return f(); }; export const types = { ark: "ark", - bitcoin: "bitcoin", - liquid: "liquid", - lightning: "lightning", - internal: "internal", - fund: "fund", - ecash: "ecash", - bolt12: "bolt12", + bitcoin: "bitcoin", + liquid: "liquid", + lightning: "lightning", + internal: "internal", + fund: "fund", + ecash: "ecash", + bolt12: "bolt12", }; export const ease = (t: number): number => - t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; + t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; export function shuffleArray(array: T[]): void { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } } export const closest = (a: number[], n: number): number => - a.reduce((prev, curr) => - Math.abs(curr - n) < Math.abs(prev - n) ? curr : prev, - ); + a.reduce((prev, curr) => + Math.abs(curr - n) < Math.abs(prev - n) ? curr : prev, + ); export const isLiquid = (text: string): boolean => - text.startsWith("Az") || - text.startsWith("lq1") || - text.startsWith("VJL") || - text.startsWith("VT") || - text.startsWith("XR") || - text.startsWith("XC") || - ((text.startsWith("H") || text.startsWith("G") || text.startsWith("Q")) && - text.length === 34) || - (text.startsWith("ert1q") && text.length === 43) || - (text.startsWith("ex1q") && text.length === 42) || - text.startsWith("el1qq") || - text.startsWith("lq1qq"); + text.startsWith("Az") || + text.startsWith("lq1") || + text.startsWith("VJL") || + text.startsWith("VT") || + text.startsWith("XR") || + text.startsWith("XC") || + ((text.startsWith("H") || text.startsWith("G") || text.startsWith("Q")) && + text.length === 34) || + (text.startsWith("ert1q") && text.length === 43) || + (text.startsWith("ex1q") && text.length === 42) || + text.startsWith("el1qq") || + text.startsWith("lq1qq"); export function getCookie(name: string): string | null { - const nameEQ = `${name}=`; - const ca = document.cookie.split(";"); - for (let i = 0; i < ca.length; i++) { - let c = ca[i]; - while (c.charAt(0) === " ") c = c.substring(1); - if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); - } - return null; + const nameEQ = `${name}=`; + const ca = document.cookie.split(";"); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === " ") c = c.substring(1); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); + } + return null; } export function setCookie(name: string, value: string, seconds: number): void { - const now = new Date(); - now.setTime(now.getTime() + seconds * 1000); - const expires = `expires=${now.toUTCString()}`; - document.cookie = `${name}=${value};${expires};path=/`; + const now = new Date(); + now.setTime(now.getTime() + seconds * 1000); + const expires = `expires=${now.toUTCString()}`; + document.cookie = `${name}=${value};${expires};path=/`; } export const network = { - bitcoin: { - bech32: "bc", - pubKeyHash: 0x00, - scriptHash: 0x05, - wif: 0x80, - }, - regtest: { - bech32: "bcrt", - pubKeyHash: 0x6f, - scriptHash: 0xc4, - wif: 0xef, - }, + bitcoin: { + bech32: "bc", + pubKeyHash: 0x00, + scriptHash: 0x05, + wif: 0x80, + }, + regtest: { + bech32: "bcrt", + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef, + }, }[PUBLIC_COINOS_NETWORK]; export const versions = { - bitcoin: { - private: 0x0488ade4, - public: 0x0488b21e, - }, - regtest: { - private: 0x04358394, - public: 0x043587cf, - }, + bitcoin: { + private: 0x0488ade4, + public: 0x0488b21e, + }, + regtest: { + private: 0x04358394, + public: 0x043587cf, + }, }[PUBLIC_COINOS_NETWORK]; export const str = (s) => s.toString(); export const ago = (t) => { - const now = new Date(); - const date = new Date(t * 1000); - const seconds = differenceInSeconds(now, date); - if (seconds < 60) return `${seconds}s`; + const now = new Date(); + const date = new Date(t * 1000); + const seconds = differenceInSeconds(now, date); + if (seconds < 60) return `${seconds}s`; - const minutes = differenceInMinutes(now, date); - if (minutes < 60) return `${minutes}m`; + const minutes = differenceInMinutes(now, date); + if (minutes < 60) return `${minutes}m`; - const hours = differenceInHours(now, date); - if (hours < 24) return `${hours}h`; + const hours = differenceInHours(now, date); + if (hours < 24) return `${hours}h`; - const days = differenceInDays(now, date); - return `${days}d`; + const days = differenceInDays(now, date); + return `${days}d`; }; -export const register = async (user, ip, cookies, loginRedirect, host?, extraHeaders?: Record) => { - let error; - - const headers: Record = { - ...extraHeaders, - }; - if (ip) headers["cf-connecting-ip"] = ip; - - let sk; - try { - ({ sk } = await post("/register", { user }, headers)); - } catch (e) { - ({ message: error } = e as Error); - } - - try { - await login(user, cookies, ip, host, extraHeaders); - error = null; - } catch (e) { - const { message } = e as Error; - error ||= message; - } - - const expires = new Date(); - expires.setSeconds(expires.getSeconds() + 21000000); - cookies.set("sk", sk, { - path: "/", - expires, - httpOnly: false, - sameSite: "lax", - }); - - if (error) return svelteFail(400, { error }); - redirect(303, loginRedirect || `/${user.username}`); +export const register = async ( + user, + ip, + cookies, + loginRedirect, + host?, + extraHeaders?: Record, +) => { + let error; + + const headers: Record = { + ...extraHeaders, + }; + if (ip) headers["cf-connecting-ip"] = ip; + + let sk; + try { + ({ sk } = await post("/register", { user }, headers)); + } catch (e) { + ({ message: error } = e as Error); + } + + try { + await login(user, cookies, ip, host, extraHeaders); + error = null; + } catch (e) { + const { message } = e as Error; + error ||= message; + } + + const expires = new Date(); + expires.setSeconds(expires.getSeconds() + 21000000); + cookies.set("sk", sk, { + path: "/", + expires, + httpOnly: false, + sameSite: "lax", + }); + + if (error) return svelteFail(400, { error }); + redirect(303, loginRedirect || `/${user.username}`); }; -- cgit v1.2.3