Remove NWC, return a Ln-Invoice header on the zap endpoint
This commit is contained in:
parent
a690fa3096
commit
78044cc8b6
|
@ -15,7 +15,7 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
|
import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
|
||||||
import { getLnurl } from '@/utils/lnurl.ts';
|
import { getInvoice, getLnurl } from '@/utils/lnurl.ts';
|
||||||
import { lookupPubkey } from '@/utils/lookup.ts';
|
import { lookupPubkey } from '@/utils/lookup.ts';
|
||||||
import { addTag, deleteTag } from '@/utils/tags.ts';
|
import { addTag, deleteTag } from '@/utils/tags.ts';
|
||||||
import { asyncReplaceAll } from '@/utils/text.ts';
|
import { asyncReplaceAll } from '@/utils/text.ts';
|
||||||
|
@ -450,15 +450,16 @@ const zapController: AppController = async (c) => {
|
||||||
const author = target?.author;
|
const author = target?.author;
|
||||||
const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content);
|
const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content);
|
||||||
const lnurl = getLnurl(meta);
|
const lnurl = getLnurl(meta);
|
||||||
|
const amount = params.data.amount;
|
||||||
|
|
||||||
if (target && lnurl) {
|
if (target && lnurl) {
|
||||||
await createEvent({
|
const nostr = await createEvent({
|
||||||
kind: 9734,
|
kind: 9734,
|
||||||
content: params.data.comment ?? '',
|
content: params.data.comment ?? '',
|
||||||
tags: [
|
tags: [
|
||||||
['e', target.id],
|
['e', target.id],
|
||||||
['p', target.pubkey],
|
['p', target.pubkey],
|
||||||
['amount', params.data.amount.toString()],
|
['amount', amount.toString()],
|
||||||
['relays', Conf.relay],
|
['relays', Conf.relay],
|
||||||
['lnurl', lnurl],
|
['lnurl', lnurl],
|
||||||
],
|
],
|
||||||
|
@ -467,7 +468,11 @@ const zapController: AppController = async (c) => {
|
||||||
const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() });
|
const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() });
|
||||||
status.zapped = true;
|
status.zapped = true;
|
||||||
|
|
||||||
return c.json(status);
|
return c.json(status, {
|
||||||
|
headers: {
|
||||||
|
'Ln-Invoice': await getInvoice({ amount, nostr, lnurl }, signal),
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return c.json({ error: 'Event not found.' }, 404);
|
return c.json({ error: 'Event not found.' }, 404);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { NKinds, NostrEvent, NPolicy, NSchema as n } from '@nostrify/nostrify';
|
import { NKinds, NostrEvent, NPolicy, NSchema as n } from '@nostrify/nostrify';
|
||||||
import { LNURL } from '@nostrify/nostrify/ln';
|
|
||||||
import { PipePolicy } from '@nostrify/nostrify/policies';
|
import { PipePolicy } from '@nostrify/nostrify/policies';
|
||||||
import Debug from '@soapbox/stickynotes/debug';
|
import Debug from '@soapbox/stickynotes/debug';
|
||||||
import { sql } from 'kysely';
|
import { sql } from 'kysely';
|
||||||
|
@ -12,15 +11,12 @@ import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { DVM } from '@/pipeline/DVM.ts';
|
import { DVM } from '@/pipeline/DVM.ts';
|
||||||
import { MuteListPolicy } from '@/policies/MuteListPolicy.ts';
|
import { MuteListPolicy } from '@/policies/MuteListPolicy.ts';
|
||||||
import { RelayError } from '@/RelayError.ts';
|
import { RelayError } from '@/RelayError.ts';
|
||||||
import { hydrateEvents, purifyEvent } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { eventAge, nostrDate, nostrNow, parseNip05, Time } from '@/utils.ts';
|
import { eventAge, nostrDate, parseNip05, Time } from '@/utils.ts';
|
||||||
import { fetchWorker } from '@/workers/fetch.ts';
|
|
||||||
import { policyWorker } from '@/workers/policy.ts';
|
import { policyWorker } from '@/workers/policy.ts';
|
||||||
import { TrendsWorker } from '@/workers/trends.ts';
|
import { TrendsWorker } from '@/workers/trends.ts';
|
||||||
import { verifyEventWorker } from '@/workers/verify.ts';
|
import { verifyEventWorker } from '@/workers/verify.ts';
|
||||||
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
|
||||||
import { lnurlCache } from '@/utils/lnurl.ts';
|
|
||||||
import { nip05Cache } from '@/utils/nip05.ts';
|
import { nip05Cache } from '@/utils/nip05.ts';
|
||||||
import { updateStats } from '@/utils/stats.ts';
|
import { updateStats } from '@/utils/stats.ts';
|
||||||
import { getTagSet } from '@/utils/tags.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
|
@ -48,7 +44,6 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise<void
|
||||||
DVM.event(event),
|
DVM.event(event),
|
||||||
trackHashtags(event),
|
trackHashtags(event),
|
||||||
processMedia(event),
|
processMedia(event),
|
||||||
payZap(event, signal),
|
|
||||||
streamOut(event),
|
streamOut(event),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -189,53 +184,6 @@ function processMedia({ tags, pubkey, user }: DittoEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Emit Nostr Wallet Connect event from zaps so users may pay. */
|
|
||||||
async function payZap(event: DittoEvent, signal: AbortSignal) {
|
|
||||||
if (event.kind !== 9734) return;
|
|
||||||
|
|
||||||
const lnurl = event.tags.find(([name]) => name === 'lnurl')?.[1];
|
|
||||||
const amount = Number(event.tags.find(([name]) => name === 'amount')?.[1]);
|
|
||||||
|
|
||||||
if (!lnurl || !amount) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const details = await lnurlCache.fetch(lnurl, { signal });
|
|
||||||
|
|
||||||
if (details.tag !== 'payRequest' || !details.allowsNostr || !details.nostrPubkey) {
|
|
||||||
throw new Error('invalid lnurl');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (amount > details.maxSendable || amount < details.minSendable) {
|
|
||||||
throw new Error('amount out of range');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { pr } = await LNURL.callback(
|
|
||||||
details.callback,
|
|
||||||
{ amount, nostr: purifyEvent(event), lnurl },
|
|
||||||
{ fetch: fetchWorker, signal },
|
|
||||||
);
|
|
||||||
|
|
||||||
const signer = new AdminSigner();
|
|
||||||
|
|
||||||
const nwcRequestEvent = await signer.signEvent({
|
|
||||||
kind: 23194,
|
|
||||||
content: await signer.nip04.encrypt(
|
|
||||||
event.pubkey,
|
|
||||||
JSON.stringify({ method: 'pay_invoice', params: { invoice: pr } }),
|
|
||||||
),
|
|
||||||
created_at: nostrNow(),
|
|
||||||
tags: [
|
|
||||||
['p', event.pubkey],
|
|
||||||
['e', event.id],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await handleEvent(nwcRequestEvent, signal);
|
|
||||||
} catch (e) {
|
|
||||||
debug('lnurl error:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Determine if the event is being received in a timely manner. */
|
/** Determine if the event is being received in a timely manner. */
|
||||||
function isFresh(event: NostrEvent): boolean {
|
function isFresh(event: NostrEvent): boolean {
|
||||||
return eventAge(event) < Time.seconds(10);
|
return eventAge(event) < Time.seconds(10);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Debug from '@soapbox/stickynotes/debug';
|
||||||
import { SimpleLRU } from '@/utils/SimpleLRU.ts';
|
import { SimpleLRU } from '@/utils/SimpleLRU.ts';
|
||||||
import { Time } from '@/utils/time.ts';
|
import { Time } from '@/utils/time.ts';
|
||||||
import { fetchWorker } from '@/workers/fetch.ts';
|
import { fetchWorker } from '@/workers/fetch.ts';
|
||||||
|
import { NostrEvent } from '@nostrify/nostrify';
|
||||||
|
|
||||||
const debug = Debug('ditto:lnurl');
|
const debug = Debug('ditto:lnurl');
|
||||||
|
|
||||||
|
@ -38,4 +39,32 @@ function getLnurl({ lud06, lud16 }: { lud06?: string; lud16?: string }, limit?:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getLnurl, lnurlCache };
|
interface CallbackParams {
|
||||||
|
amount: number;
|
||||||
|
nostr: NostrEvent;
|
||||||
|
lnurl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInvoice(params: CallbackParams, signal?: AbortSignal): Promise<string> {
|
||||||
|
const { amount, lnurl } = params;
|
||||||
|
|
||||||
|
const details = await lnurlCache.fetch(lnurl, { signal });
|
||||||
|
|
||||||
|
if (details.tag !== 'payRequest' || !details.allowsNostr || !details.nostrPubkey) {
|
||||||
|
throw new Error('invalid lnurl');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount > details.maxSendable || amount < details.minSendable) {
|
||||||
|
throw new Error('amount out of range');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pr } = await LNURL.callback(
|
||||||
|
details.callback,
|
||||||
|
params,
|
||||||
|
{ fetch: fetchWorker, signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
return pr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getInvoice, getLnurl, lnurlCache };
|
||||||
|
|
Loading…
Reference in New Issue