diff --git a/src/lib/components/CmsDesign.svelte b/src/lib/components/CmsDesign.svelte index f1a0bbeff..3ef1f96c9 100644 --- a/src/lib/components/CmsDesign.svelte +++ b/src/lib/components/CmsDesign.svelte @@ -182,6 +182,12 @@ : ''}{token.height ? `height: ${token.height}px;` : ''}" /> + {:else if token.type === 'qrCode'} + {#if token.slug === 'Bolt12'} + + QR code + + {/if} {:else if token.type === 'currencyCalculatorWidget'} {:else if token.type === 'html'} @@ -258,6 +264,16 @@ /> {:else if token.type === 'pictureWidget'} + {:else if token.type === 'qrCode'} + {#if token.slug === 'Bolt12'} + + QR code + + {/if} {:else if token.type === 'currencyCalculatorWidget'} {:else if token.type === 'html'} diff --git a/src/lib/server/cms.ts b/src/lib/server/cms.ts index bdd49bb41..f4774ca13 100644 --- a/src/lib/server/cms.ts +++ b/src/lib/server/cms.ts @@ -77,6 +77,7 @@ type TokenObject = display: string | undefined; raw: string; } + | { type: 'qrCode'; slug: string; raw: string } | { type: 'currencyCalculatorWidget'; slug: string; raw: string }; export async function cmsFromContent( @@ -100,6 +101,7 @@ export async function cmsFromContent( /\[TagProducts=(?[\p{L}\d_-]+)(?:[?\s]display=(?[a-z0-9-]+))?\]/giu; const GALLERY_WIDGET_REGEX = /\[Gallery=(?[\p{L}\d_-]+)(?:[?\s]display=(?[a-z0-9-]+))?\]/giu; + const QRCODE_REGEX = /\[QRCode=(?[\p{L}\d_-]+)\]/giu; const CURRENCY_CALCULATOR_WIDGET_REGEX = /\[CurrencyCalculator=(?[a-z0-9-]+)\]/giu; const productSlugs = new Set(); @@ -112,6 +114,7 @@ export async function cmsFromContent( const countdownFormSlugs = new Set(); const tagProductsSlugs = new Set(); const gallerySlugs = new Set(); + const qrCodeSlugs = new Set(); const currencyCalculatorSlugs = new Set(); const tokens: { @@ -143,6 +146,7 @@ export async function cmsFromContent( ...matchAndSort(content, COUNTDOWN_WIDGET_REGEX, 'countdownWidget'), ...matchAndSort(content, TAG_PRODUCTS_REGEX, 'tagProducts'), ...matchAndSort(content, GALLERY_WIDGET_REGEX, 'galleryWidget'), + ...matchAndSort(content, QRCODE_REGEX, 'qrCode'), ...matchAndSort(content, CURRENCY_CALCULATOR_WIDGET_REGEX, 'currencyCalculatorWidget') ].sort((a, b) => (a.index ?? 0) - (b.index ?? 0)); for (const match of matches) { @@ -250,6 +254,14 @@ export async function cmsFromContent( raw: match[0] }); break; + case 'qrCode': + qrCodeSlugs.add(match.groups.slug); + token.push({ + type: 'qrCode', + slug: match.groups.slug, + raw: match[0] + }); + break; case 'currencyCalculatorWidget': currencyCalculatorSlugs.add(match.groups.slug); token.push({ diff --git a/src/lib/server/locks/nostr-notifications.ts b/src/lib/server/locks/nostr-notifications.ts index 66130a373..f2ff05e97 100644 --- a/src/lib/server/locks/nostr-notifications.ts +++ b/src/lib/server/locks/nostr-notifications.ts @@ -95,7 +95,7 @@ function initRelayPool() { [ // Messages sent to us { - kinds: [Kind.EncryptedDirectMessage, Kind.Text], + kinds: [Kind.EncryptedDirectMessage, Kind.Text, Kind.Zap], '#p': [nostrPublicKeyHex] } ], @@ -109,7 +109,7 @@ function initRelayPool() { if (!event.tags.some((tag) => tag[0] === 'p' && tag[1] === nostrPublicKeyHex)) { return; } - if (![Kind.EncryptedDirectMessage, Kind.Text].includes(event.kind)) { + if (![Kind.EncryptedDirectMessage, Kind.Text, Kind.Zap].includes(event.kind)) { return; } @@ -222,6 +222,34 @@ async function handleNostrNotification(nostrNotification: NostRNotification): Pr sig: '' } satisfies Event; } + + if (nostrNotification.kind === Kind.Zap) { + const npub = nostrNotification.dest; + + if (!npub) { + return; + } + + const receiverPublicKeyHex = nostrToHex(npub); + + return { + id: '', + content: await nip04.encrypt(nostrPrivateKeyHex, receiverPublicKeyHex, content), + created_at: getUnixTime( + max([ + nostrNotification.minCreatedAt ?? nostrNotification.createdAt, + nostrNotification.createdAt + ]) + ), + pubkey: nostrPublicKeyHex, + tags: [ + ['p', receiverPublicKeyHex], + ['bootikVersion', String(NOSTR_PROTOCOL_VERSION)] + ], + kind: Kind.Zap, + sig: '' + } satisfies Event; + } })(); if (!event) { diff --git a/src/lib/server/orders.ts b/src/lib/server/orders.ts index ebe62a377..2aacdb215 100644 --- a/src/lib/server/orders.ts +++ b/src/lib/server/orders.ts @@ -1073,8 +1073,8 @@ async function generatePaymentInfo(params: { const invoice = await phoenixdCreateInvoice(satoshis, label, params.orderId); return { - address: invoice.paymentAddress, - invoiceId: invoice.paymentHash, + address: invoice.payment_address, + invoiceId: invoice.r_hash, processor: 'phoenixd' }; } else { diff --git a/src/lib/server/phoenixd.ts b/src/lib/server/phoenixd.ts index 47cd7e777..68a2d4fd7 100644 --- a/src/lib/server/phoenixd.ts +++ b/src/lib/server/phoenixd.ts @@ -34,6 +34,37 @@ export async function phoenixdBalance(): Promise<{ balanceSat: number; feeCredit return await res.json(); } +export async function phoenixdGetBolt12(): Promise { + const res = await fetch(`${runtimeConfig.phoenixd.url}/getoffer`, { + headers: { + Authorization: `Basic ${Buffer.from(`:${runtimeConfig.phoenixd.password}`).toString( + 'base64' + )}` + } + }); + + if (!res.ok) { + throw error(500, `Error fetching Bolt12 offer: ${res.status} ${res.statusText}`); + } + + return await res.text(); +} + +export async function phoenixdLnAddress(): Promise { + const res = await fetch(`${runtimeConfig.phoenixd.url}/getlnaddress`, { + headers: { + Authorization: `Basic ${Buffer.from(`:${runtimeConfig.phoenixd.password}`).toString( + 'base64' + )}` + } + }); + if (!res.ok) { + throw error(500, `Could not get lnaddress: ${res.status} ${await res.text()}`); + } + + return await res.text(); +} + export async function phoenixdDetected(url?: string): Promise { return await Promise.race([ fetch(`${url || runtimeConfig.phoenixd.url}/getinfo`).then( @@ -48,7 +79,7 @@ export async function phoenixdCreateInvoice( satoshis: number, description: string, externalId: string -): Promise<{ paymentHash: string; paymentAddress: string }> { +): Promise<{ payment_request: string; r_hash: string; payment_address: string }> { const res = await fetch(`${runtimeConfig.phoenixd.url}/createinvoice`, { method: 'POST', headers: { @@ -78,8 +109,9 @@ export async function phoenixdCreateInvoice( .parse(await res.json()); return { - paymentHash: json.paymentHash, - paymentAddress: json.serialized + payment_request: json.serialized, + r_hash: json.paymentHash, + payment_address: json.serialized }; } diff --git a/src/lib/server/runtime-config.ts b/src/lib/server/runtime-config.ts index 8dbbef1e9..fd31e73e8 100644 --- a/src/lib/server/runtime-config.ts +++ b/src/lib/server/runtime-config.ts @@ -123,7 +123,9 @@ const baseConfig = { phoenixd: { url: 'http://localhost:9740', enabled: false, - password: '' + password: '', + lnAddress: '', + bolt12Address: '' }, productActionSettings: { eShop: { diff --git a/src/lib/utils/lightningPaymentQr.ts b/src/lib/utils/lightningPaymentQr.ts new file mode 100644 index 000000000..fe5e7ed81 --- /dev/null +++ b/src/lib/utils/lightningPaymentQr.ts @@ -0,0 +1,12 @@ +import type { Currency } from '$lib/types/Currency'; +import { toBitcoins } from './toBitcoins'; + +export function lightningPaymentQrCodeString( + paymentAddress: string, + paymentAmount: number, + paymentCurrency: Currency +) { + return `lightning:${paymentAddress}?amount=${toBitcoins(paymentAmount, paymentCurrency) + .toLocaleString('en-US', { maximumFractionDigits: 8 }) + .replaceAll(',', '')}`; +} diff --git a/src/routes/(app)/admin[[hash=admin_hash]]/nostr/+page.server.ts b/src/routes/(app)/admin[[hash=admin_hash]]/nostr/+page.server.ts index 4b6c2a9ba..b75cc3fbe 100644 --- a/src/routes/(app)/admin[[hash=admin_hash]]/nostr/+page.server.ts +++ b/src/routes/(app)/admin[[hash=admin_hash]]/nostr/+page.server.ts @@ -17,6 +17,7 @@ import { setTimeout } from 'node:timers/promises'; import type { Event } from 'nostr-tools'; import { uniqBy } from '$lib/utils/uniqBy'; import { NOSTR_PROTOCOL_VERSION } from '$lib/server/locks/handle-messages'; +import { isPhoenixdConfigured, phoenixdLnAddress } from '$lib/server/phoenixd'; export function load() { return { @@ -47,7 +48,9 @@ export const actions = { : null; const lndInfo = isLightningConfigured ? await lndGetInfo() : null; - const lnAddress = lndInfo?.uris?.[0]; + const lnAddress = + lndInfo?.uris?.[0] ?? + (isPhoenixdConfigured() ? await phoenixdLnAddress().catch(() => null) : null); await collections.nostrNotifications.insertOne({ _id: new ObjectId(), diff --git a/src/routes/(app)/admin[[hash=admin_hash]]/nostr/+page.svelte b/src/routes/(app)/admin[[hash=admin_hash]]/nostr/+page.svelte index b8c7709ec..88f6d6164 100644 --- a/src/routes/(app)/admin[[hash=admin_hash]]/nostr/+page.svelte +++ b/src/routes/(app)/admin[[hash=admin_hash]]/nostr/+page.svelte @@ -125,6 +125,19 @@ +

Zaps

+ +
    + {#each data.receivedMessages.filter((mes) => mes.kind === 9735) as message} +
  • + {#if message.kind === 4} + '⚡' + {/if} + + | {JSON.stringify(message)} +
  • + {/each} +

Received messages

    diff --git a/src/routes/(app)/admin[[hash=admin_hash]]/phoenixd/+page.server.ts b/src/routes/(app)/admin[[hash=admin_hash]]/phoenixd/+page.server.ts index 26b7760f7..64ece1db5 100644 --- a/src/routes/(app)/admin[[hash=admin_hash]]/phoenixd/+page.server.ts +++ b/src/routes/(app)/admin[[hash=admin_hash]]/phoenixd/+page.server.ts @@ -5,7 +5,9 @@ import { isPhoenixdConfigured, phoenixdBalance, phoenixdDetected, + phoenixdGetBolt12, phoenixdInfo, + phoenixdLnAddress, phoenixdPayInvoice, phoenixdSendOnChain } from '$lib/server/phoenixd.js'; @@ -29,17 +31,19 @@ export const load = async () => { try { const nodeInfo = await phoenixdInfo(); const balance = await phoenixdBalance(); - + const bolt12Address = await phoenixdGetBolt12(); return { phoenixd: runtimeConfig.phoenixd, nodeInfo, - balance + balance, + bolt12Address }; } catch (err) { return { phoenixd: runtimeConfig.phoenixd, nodeInfo: null, - balance: null + balance: null, + bolt12Address: null }; } }; @@ -76,7 +80,9 @@ export const actions = { .parse(Object.fromEntries(await event.request.formData())); runtimeConfig.phoenixd.password = parsed.password; + runtimeConfig.phoenixd.lnAddress = await phoenixdLnAddress(); + runtimeConfig.phoenixd.bolt12Address = await phoenixdGetBolt12(); await collections.runtimeConfig.updateOne( { _id: 'phoenixd' }, { diff --git a/src/routes/(app)/admin[[hash=admin_hash]]/phoenixd/+page.svelte b/src/routes/(app)/admin[[hash=admin_hash]]/phoenixd/+page.svelte index 2691a1073..33f677ed0 100644 --- a/src/routes/(app)/admin[[hash=admin_hash]]/phoenixd/+page.svelte +++ b/src/routes/(app)/admin[[hash=admin_hash]]/phoenixd/+page.svelte @@ -22,6 +22,7 @@ let withdrawMode = 'bolt11' as 'bolt11' | 'bitcoin'; let defaultUrl = data.phoenixd.url || 'http://localhost:9740'; + let showBolt12 = false;

    PhoenixD

    @@ -92,6 +93,9 @@
    + {#if data.nodeInfo}
    + {#if showBolt12} +

    Bolt12 address: {data.bolt12Address}

    +

    To use it on page CMS, use this code : [QRCode=Bolt12]

    + {/if}
    diff --git a/src/routes/(app)/phoenixd/bolt12/qrcode/+server.ts b/src/routes/(app)/phoenixd/bolt12/qrcode/+server.ts new file mode 100644 index 000000000..ce4e47030 --- /dev/null +++ b/src/routes/(app)/phoenixd/bolt12/qrcode/+server.ts @@ -0,0 +1,22 @@ +import { runtimeConfig } from '$lib/server/runtime-config'; +import qrcode from 'qrcode'; + +export async function GET({}) { + try { + const bolt12Address = runtimeConfig.phoenixd.bolt12Address; + + const svgQRCode = await qrcode.toString('lightning:' + bolt12Address, { type: 'svg' }); + + return new Response(svgQRCode, { + headers: { 'content-type': 'image/svg+xml' }, + status: 200 + }); + } catch (error) { + console.error('Error on phoenixdGetBolt12:', error); + + return new Response("Erreur lors de la génération de l'adresse Bolt12", { + headers: { 'content-type': 'text/plain' }, + status: 500 + }); + } +} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 2d269b889..fa823f43b 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -38,6 +38,7 @@ export async function load(event) { viewportWidth, contactModes: runtimeConfig.contactModes, hideFromSearchEngines: runtimeConfig.hideFromSearchEngines, - ageRestriction: runtimeConfig.ageRestriction + ageRestriction: runtimeConfig.ageRestriction, + bolt12Address: runtimeConfig.phoenixd.bolt12Address }; } diff --git a/src/routes/.well-known/lnurlp/[id]/+server.ts b/src/routes/.well-known/lnurlp/[id]/+server.ts index 137a18f7d..0665d8142 100644 --- a/src/routes/.well-known/lnurlp/[id]/+server.ts +++ b/src/routes/.well-known/lnurlp/[id]/+server.ts @@ -18,14 +18,15 @@ export const OPTIONS = () => { }; export const GET = async ({ params, url }) => { - if (!isLightningConfigured) { + if (!isLightningConfigured && !runtimeConfig.phoenixd.lnAddress) { throw error(400, 'Lighting is not configured'); } + if (isLightningConfigured) { + const info = await lndGetInfo(); - const info = await lndGetInfo(); - - if (!info.uris.length) { - throw error(400, 'No public Lightning URI'); + if (!info.uris.length) { + throw error(400, 'No public Lightning URI'); + } } let picture: Buffer | null = null; diff --git a/src/routes/lightning/pay/+server.ts b/src/routes/lightning/pay/+server.ts index 8de3c4ca7..e18de8864 100644 --- a/src/routes/lightning/pay/+server.ts +++ b/src/routes/lightning/pay/+server.ts @@ -1,8 +1,10 @@ import { isLightningConfigured, lndCreateInvoice } from '$lib/server/lnd'; +import { phoenixdCreateInvoice } from '$lib/server/phoenixd'; import { runtimeConfig } from '$lib/server/runtime-config'; import { SATOSHIS_PER_BTC } from '$lib/types/Currency'; import { error } from '@sveltejs/kit'; import { jwtVerify } from 'jose'; +import { ObjectId } from 'mongodb'; import { z } from 'zod'; export const OPTIONS = () => { @@ -16,21 +18,25 @@ export const OPTIONS = () => { }; export const GET = async ({ url }) => { - if (!isLightningConfigured) { + if (!isLightningConfigured && !runtimeConfig.phoenixd.lnAddress) { throw error(400, 'Lightning is not configured'); } - const { amount, metadata: metadataJwt } = z + const { + amount, + metadata: metadataJwt, + comment + } = z .object({ amount: z .number({ coerce: true }) .int() .min(1) .max(SATOSHIS_PER_BTC * 1000), - metadata: z.string() + metadata: z.string(), + comment: z.string().default('Zap !') }) .parse(Object.fromEntries(url.searchParams)); - const result = await jwtVerify( metadataJwt, Buffer.from(runtimeConfig.lnurlPayMetadataJwtSigningKey) @@ -41,11 +47,12 @@ export const GET = async ({ url }) => { metadata: z.string() }) .parse(result.payload); - - const invoice = await lndCreateInvoice(amount, { - descriptionHash: await crypto.subtle.digest('SHA-256', new TextEncoder().encode(metadata)), - milliSatoshis: true - }); + const invoice = isLightningConfigured + ? await lndCreateInvoice(amount, { + descriptionHash: await crypto.subtle.digest('SHA-256', new TextEncoder().encode(metadata)), + milliSatoshis: true + }) + : await phoenixdCreateInvoice(amount / 1000, comment, new ObjectId().toString()); return new Response( JSON.stringify({